DEV Community

Cover image for Stalk a Click through a Re-Frame/actix_web App
Ben Lovy
Ben Lovy

Posted on

Stalk a Click through a Re-Frame/actix_web App

Rollin' on 20s

Why

I don't see a ton of material about either of these amazing tools, especially at the beginner level, and this is a solid way to get an overview of all the important bits.

The Thing

It's a toy dice roller, hosted in this github repo. The user can submit either regular old dice rolls: 1d6, 2d10, or define their awn aliases. For example, you could save 3d8 to goblin and then just type goblin. It also accepts a multiplier to repeat the roll: 2 2d6 or 27 goblin. Multiple commands are run in sequence: 2d9 goblin is fine as a single command and will run both.

The Tools

The backend uses actix_web to connect to a SQLite database for storing the aforementioned aliases. The frontend is implemented with the Re-Frame framework which is built on top of Reagent, a method of defining React components in ClojureScript. Re-Frame provides a functional state management solution - you're on your own for that with plain Reagent (not unlike React.js).

The Journey

I'm going to examine just one pathway end to end - the click to submit a roll command. We'll assume we've got the above example alias already defined: goblin: 3d8. I'm not gonna throw 27 goblins at you on your first go, though, I'm not Satan...let's try the command 3 goblin and see how we fare.

I'm zeroing in on Re-Frame and actix_web stuff, so some function bodies will be omitted in the interest of time. This post managed to get pretty long anyway without digging into every line! I'll throw in links to relevant files throughout for the full context.

Gather the input

Re-Frame provides a rigid structure for defining different parts of your application. We'll jump through a few of them, but our click's journey starts (as many do) with a textbox and a button. All UI code is located in views.cljs.

Reagent, if you're unfamiliar, is awesome. Seriously, I'm even giving you the link again. It takes a lot of ceremony out of React and distills components to their core. Each component is a function, and it emulates hiccup's syntax to allow you to define your HTML in the form of succinct Clojure vectors.

You use keywords like :div - a [:div] vector will expand to <div></div>. Everything else is a child of that element. Each vector optionally takes an options map as above, or an even quicker shorthand: [:span#firstName.name.focus "SPAN!"] expands to <span id=\"firstName\" class=\"name focus\">SPAN!</span>. Lisps with all their tree-ness right out in the open like that are natural choices for representing and manipulating tree structures like the DOM. Perfect for prototyping React apps!

Here's the specific component:

(defn command-input
  "Command input"
  []
  [:div
   "Command: "
   [:input {:type "text"
            :id "field"
            :name "cmd"}]
   [:input {:type "button"
            :value "Submit"
            :on-click #(re-frame/dispatch
                        [::events/submit-command (-> (.getElementById js/document "field") .-value)])}]])
Enter fullscreen mode Exit fullscreen mode

Potentially unfamiliar Clojure-ness aside, this is pretty easy to read. When called in a Reagent tree, this funciton is a Reagent component that defines an input textboxes and a button, similar to code you'd write using any frontend tool.

Our trip starts (as so many do) when the user has entered some stuff and clicks the button. The behavior is in the click handler:

#(re-frame/dispatch [::events/submit-command (-> (.getElementById js/document "field") .-value)])`
Enter fullscreen mode Exit fullscreen mode

#() defines an anonymous function in Clojure. In JavaScript this looks like () => {/* stuff */}. Any arguments are %, %2, %3 etc if used: #(%) => (fn [arg1] (arg1)). This one doesn't have any.

JS interop is dirt simple in ClojureScript. We're calling document.getElementById('field'), rearranged so that Clojure-style the function is in the first position of the s-expression. Subsequent arugments would follow "field". It's really that easy. To access the value property of that element, you use the .-value syntax - otherwise CLJS will think you're trying to call a method value().

This snippet uses the thread macro ->, which works like a pipe. It lets you write chained operations without nesting parens too deeply, which Lisps are notorious for. Perhaps unnecessary with just two operations, but I find this more readable and consistency is always nice.

Enter Re-Frame

This submit-command event is defined along with all the other events this application deals with, in events.cljs. Nice and neat. This is what I love so much about working with Re-Frame. Once you get your head around the model which is not as complicated as it sounds at first it's always unambiguous where any new code should go. It's also got one of the best READMEs on GitHub, but that's just, like, my opinion, man.

Notice how we're not actually calling a function here to handle the event - we're passing a data structure containing the name of our event to re-frame/dispatch which is going to handle that for us in FIFO order. Lets look at this event specifically:

(re-frame/reg-event-fx
 ::submit-command
 (fn-traced [_ [_ cmd]]
   {:http-xhrio {:method :get
                 :uri (str "http://localhost:8080/roll/" (clojure.string/replace cmd #" " "/"))
                 :timeout 8000
                 :response-format (ajax/json-response-format {:keywords? true})
                 :on-success [::save-roll]
                 :on-failure [::bad-http-result]}}))
Enter fullscreen mode Exit fullscreen mode

You create an event by registering its effects for the dispatcher with the aptly named reg-event-fx function. Notice how we just give it the name and then immediately open a fn - not unlike the defn macro. fn-traced just allows this event to plug in to the excellent re-frame-10x devtools - it's just a lambda otherwise.

The arguments to the fn are [cofx event]. We're not using any co-effects yet. We will, don't fret, but for this event I'm ignoring them with _. The event argument is then destructured. Remember the event vector? We made it ourselves a moment ago: [::events/submit-command (-> (.getElementById js/document "field") .-value)]. That first part is just the name of the event, which we don't need - there's another _ - and we're storing whatever the user entered as cmd.

This event leverages the officially supported http-fx library for performing AJAX requests. This library provides the :http-xhrio effect handler. This is also very straightforward to use - you pass it an options map with the request you're making. It's got all the parts you'd expect to need to define.

Our specific cmd of 3 goblin shows up in the URI, no surprises there. We replace the space with a /: http://localhost:8080/roll/3/goblins.

This library has you specify the formats you're using - we're going JSON all the way.

Also of note is that we define both what happens on success (200) or on failure (anything else). Both of these are simply other events defined in the same source file. The Re-Frame dispatcher will call the proper follow-up once the response comes back.

However, before we can take a look at that, we've gotta actually generate the response! Let's head on over to the backend and take a look at that GET /roll handler.

Back of the House

The whole outline of our server is defined in main.rs, beginning on line 78.

Actix comes with built-in support for CORS - any resource registered in this initial setup will gain the correct behavior automatically. As with many Rust APIS, we're using a builder pattern to define the configuration of the app. Once all the configuration is done, we finish it off with register(). The resource in question is on line 89:

.resource("/roll/{tail:.*}", |r| {r.method(http::Method::GET).with(roll)})
Enter fullscreen mode Exit fullscreen mode

This defines the endpoint, specifies the method, and calls the specific handler roll. {tail:.*} means that anything in the URL after roll/ will be passed to the handler in the request as tail. When a request hits the server, it tries each resource defined in succession. If it matches this endpoint and method, this handler will be called from handlers.rs:

// GET /roll/{cmd}
pub fn roll(req: HttpRequest) -> impl Responder {
    let cmd = &req.match_info()["tail"];
    let cmds = ((&cmd)
        .split('/')
        .collect::<Vec<&str>>()
        .iter()
        .map(|s| s.to_string()))
        .collect::<Vec<String>>();
    roll_strs(&cmds)
}
Enter fullscreen mode Exit fullscreen mode

I find actix_web extraordinarily ergonomic. For one, it was an early embracer of the fancy-pants impl Trait syntax there in the return type. In order to work as a handler, your function just needs to return any type that implements the Responder trait, and actix_web provides many out of the box, like for String and even Json (it's got serde baked in). Alternatively you can implement it yourself like we're about to do.

After getting 3/goblin with &req.match_info("tail"), we just turn it into vec!["3", "goblin"] and pass it to roll_strs(). This is our return value for roll(), so we know whatever this function returns will implement Responder.

The Meat 'n' Potatoes

As promised roll_strs() returns a custom type, Outcomes, for which it's necessary to manually implement Responder:

#[derive(Serialize)]
pub struct Outcomes {
    pub outcomes: Vec<Outcome>,
}

pub fn roll_strs(s: &[String]) -> Outcomes {
    validate_input(s).unwrap().run()
}
Enter fullscreen mode Exit fullscreen mode

The custom type is just a wrapper for a Vec<Outcome>. This is an Outcome:

#[derive(Clone, Debug, Serialize)]
pub struct Outcome {
    roll: String,
    rolls: Vec<u32>,
}
Enter fullscreen mode Exit fullscreen mode

These structs define the shape of our JSON response. The response for 3 goblin will be shaped like this:

{"outcomes":
    [
        {"roll":"3d8","rolls":[6,1,3]},
        {"roll":"3d8","rolls":[7,1,8]},
        {"roll":"3d8","rolls":[8,1,5]}
    ]
}
Enter fullscreen mode Exit fullscreen mode

Responder is not a difficult trait to implement. It's only got one function, respond_to:

impl Responder for Outcomes {
    type Item = HttpResponse;
    type Error = Error;

    fn respond_to<S>(self, _req: &HttpRequest<S>) -> Result<HttpResponse, Error> {
        let body = serde_json::to_string(&self)?;

        Ok(HttpResponse::Ok()
            .content_type("application/json")
            .body(body))
    }
}
Enter fullscreen mode Exit fullscreen mode

We can easily create Json from our custom types because of the Serialize trait we auto-derived - all it takes is serde_json::to_string(&Outcomes)?. Then we build a successful HttpResponse, give it the expected Content-Type, and include our JSON string as the response body. If we had been unable to build the json for whatever reason, the ? at the end of serde_json::to_string() would have returned an actix_web::Error - this will also result in an HttpResponse going back to the user, but with an unsucessful code.

For brevity's sake I'll skip the machinery - there's nothing revolutionary about getting an Outcome from an input like 3d8 in Rust. It's all housed in roll.rs for the curious.

First, though, we've gotta grab 3d8 from goblin, and know to roll it three times. The body of roll_strs calls runs us through the goodies in command.rs first. Let's take a look.

Command Parsing

First, we validate_input(s). Here's the signature - nothing fancy in the body:

pub fn validate_input(s: &[String]) -> io::Result<Command> {
   // parsing with regular expressions
}
Enter fullscreen mode Exit fullscreen mode

In short, we look at the series of strings passed in and try to return a Command:

#[derive(Debug, PartialEq)]
pub enum Command {
    Roll(Vec<Roll>),              // One or more XdX args
    Multiplier(u32, Vec<String>), // an integer repeater, and then either rolls or lookups
    Lookup(Vec<String>),          // we get the roll from the db, there shouldn't be anything else
}
Enter fullscreen mode Exit fullscreen mode

Our 3 goblin example got parsed by validate_input() to Command::Multiplier(3, vec!["goblin"]), which will in turn run a Lookup("goblin") three times. Back up in roll_strs() we end things off by calling run() on the returned command. This method returns our Outcomes:

impl Command {
    pub fn run(&self) -> Outcomes {
        match self {
            // a branch for each Command variant
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Multiplier isn't terribly interesting - it'll run the Lookup command here three times, and the returned Outcomes will contain all three results. Let's instead jump right to (the important parts of) Lookup:

Command::Lookup(ids) => {
                let conn = DB_POOL
                    .get()
                    .expect("Could not get db conn from thread pool");
                let items = get_items(&conn);
                let mut ret = Vec::new();
                for id in ids {
                    // look for each passed in item in the returned db items
                    // if found, get an Outcome from the associated roll and push it to ret
                    // log output
                }
                Outcomes { outcomes: ret } // return an Outcomes struct
Enter fullscreen mode Exit fullscreen mode

Goblin Hunting

Before we can interact with the database, we need to get a connection. I'm using the r2d2 crate to maintain a pool of open database connections instead of creating a new one for each request. Here's the relevant code from db.rs:

lazy_static! {
    pub static ref DB_POOL: Pool = init_pool();
}

pub type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>;

pub const DATABASE_URL: &str = dotenv!("DATABASE_URL");

pub fn init_pool() -> Pool {
    let manager = ConnectionManager::<SqliteConnection>::new(DATABASE_URL);
    r2d2::Pool::new(manager).expect("failed to create pool")
}
Enter fullscreen mode Exit fullscreen mode

This is standard r2d2 boilerplate that sets up a static DB_POOL using the database location defined in a .env file in the project folder. To grab a connection, we use let conn = DB_POOL.get(). One nice thing is that when conn goes out of scope at the end of this block the connection will be automatically returned to the pool for us. We don't have to do anything about it ourselves.

Now we can call get_items(&conn) using this db connection. I'm using the diesel ORM:

pub fn get_items(conn: &SqliteConnection) -> Items {
    use schema::items::dsl::*;
    let results = items
        .limit(5)
        .load::<Item>(conn)
        .expect("Error loading items");

    let mut ret = Vec::new();
    for item in results {
        ret.push(item);
    }
    Items { items: ret }
}
Enter fullscreen mode Exit fullscreen mode

The Items return type is a wrapper struct for a Vec<Item>. The Item looks like this:

#[derive(Debug, Queryable, Serialize)]
pub struct Item {
    pub id: i32,
    pub title: String,
    pub damage: String,
}
Enter fullscreen mode Exit fullscreen mode

This exactly matches our database schema. Diesel provides the Queryable trait, meaning it can marshall entries in our SQLite database to this Rust type for us automatically. The items table was created with the following SQL:

CREATE TABLE items (
       id INTEGER NOT NULL PRIMARY KEY,
       title VARCHAR NOT NULL,
       damage TEXT NOT NULL
)
Enter fullscreen mode Exit fullscreen mode

Diesel creates a DSL for us to compose queries using a Rustic API. It's quite easy to use.

This particular example is grabbing all the items from the database, because the Lookup command may have multiple strings to look for. This is pretty bad design (cough Ben cough). I could optimize it to use syntax like this if there's only one:

let results = items
        .filter(title.eq(lookup_title))
        .load::<Item>(conn)
        .expect("Error loading items");
Enter fullscreen mode Exit fullscreen mode

This would run a SELECT * FROM items WHERE title = lookup_title.

Bringing it all together, our Lookup for goblin returns something like:

Item {
    id: 1,
    title: "goblin",
    damage: "3d8",
}
Enter fullscreen mode Exit fullscreen mode

The rest of the Lookup block in Command::run() just grabs that damage field and executes the roll, saving the result for the response. Here's the example output again:

{"outcomes":
    [
        {"roll":"3d8","rolls":[6,1,3]},
        {"roll":"3d8","rolls":[7,1,8]},
        {"roll":"3d8","rolls":[8,1,5]}
    ]
}
Enter fullscreen mode Exit fullscreen mode

Back Up Front

Whew! Rust's bit is done - having found our database entry and used our custom Responder implementation to send back a JSON response, we've got to display it back the the user.

Recall that back in our Re-Frame event we defined both an effect for :on-success and :on-failure. This roll was a booming success, so when this response comes back the Re-Frame dispatcher will trigger the ::save-roll event back in events.cljs:

(re-frame/reg-event-fx
 ::save-roll
 [(re-frame/inject-cofx :now) (re-frame/inject-cofx :temp-id)]
 (fn-traced [{:keys [db temp-id now]} [_ result]]
            {:db (update db :roll-hx conj {:id temp-id :time now :result result})}))
Enter fullscreen mode Exit fullscreen mode

It's our good old friend reg-event-fx again, but this time there's a little bit more going on. Remember when I mentioned and then completely dropped the concept of co-effects? Before we open the lambda, we use re-frame/inject-cofx to add a little more data to the context reg-event-fx has available to work with than just the application db. In Clojure, eveything is just data. Kind of like before when the event passed in was just the vector we created, which could be destructured, cofx is just a Clojure map. By default it contains our app's db, but we have the opportunity to put anything we want on it. It's a much fancier name than concept, but I have to concede its a pretty accurate name. Let's look at :now, the first co-effect we're injecting:

(re-frame/reg-cofx
 :now
 (fn-traced [cofx _data]
            (assoc cofx :now (js/Date.))))
Enter fullscreen mode Exit fullscreen mode

It looks not altogether unike reg-event-fx. Essentially all it does is add a key to our cofx map with the key :now, and giving it the current date for a value.

Now, instead of blowing past it with an underscore, we destructure the cofx as well as the event:

[{:keys [db temp-id now]} [_ result]]
Enter fullscreen mode Exit fullscreen mode

The second part, [_ result], is exactly what we did earlier with cmd - the first element of the vector is the name of the event (::save-roll), which we don't need, and result will hold the JSON we just generated in the backend. The first part is our newly augmented cofx map. We're specifically grabbing the values of the keys specified. db is there already for us to use and represents the app state, and now is what we just injected - it's the current date. temp-id is the other co-effect I registered - feel free to check it out in events.cljs. It just allows us to assign session-local unique incrementing IDs to each incoming result by bumping an atom each time it's injected.

The body of this event just attaches a map containing this result along with the date and tempID our co-effects generated to the :roll-hx key in our app db using conj: {:id temp-id :time now :result result}.

Bringing it on home

The rest happens automagically. That's the end of our call chain - I don't have more code to follow. We did, though, change the database. Re-Frame's got it from here - it will handle to page re-render picking up our newly augmented :roll-hx because we have a component subscribed to it.

Here's our main panel:

(defn main-panel []
(let [result (re-frame/subscribe [::subs/results])
      error (re-frame/subscribe [::subs/error])
      items (re-frame/subscribe [::subs/items])]
  [:div
   [:h1 "ROLL"]
   [usage]
   "Roll history:  " [roll-hx @result] [:br]
   [command-input] [:br]
   "Items: " [all-items @items] [:br]
   [add-item] [:br]
   [view-error @error] [:hr]
   [footer]]))
Enter fullscreen mode Exit fullscreen mode

The component in question is [roll-hx @result]. This result is created up in the let binding using re-frame/subscribe. All of our subscriptions live in subs.cljs. Here's ::subs/results:

(re-frame/reg-sub
 ::results
 (fn [db]
   (:roll-hx db)))
Enter fullscreen mode Exit fullscreen mode

Couldn't be simpler - it just returns the value of the :roll-hx key from our database. When the app starts, we initialize this db as defined in db.cljs:

(def default-db
  {:name "re-frame"
   :roll-hx []
   :items []})
Enter fullscreen mode Exit fullscreen mode

Our ::save-roll event had the effect of attaching a new map to the :roll-hx. Now it looks something like:

{:name "re-frame"
   :roll-hx [{:id 0
              :time (js/Date.)
              :result {:outcomes [
        {:roll "3d8" :rolls [6,1,3]},
        {:roll "3d8" :rolls [7,1,8]},
        {:roll "3d8" :rolls [8,1,5]}
    ]]}
   ]}}]
   :items []})
Enter fullscreen mode Exit fullscreen mode

Because the view is subscribed to the :roll-hx key of our database, it will automatically redraw to display the new data. This is nice because the component doesn't need to know about the structure of your database - it's only concerned with that particular key. If the database structure changes as you develop, you'd change the subscription logic - your view doesn't need to care.

There's nothing too surprising in the actual view - it renders this data as a list. I won't go through the whole tree, it's pretty trivial stuff - there's no state here, it simply reflects the app db. Here's the outer layer:

(defn roll-hx
  "View full roll history"
  [hx]
  [:ul.hx
   (for [os (reverse hx)]
     ^{:key (:id os)}
  [:li [outcomes os]])])
Enter fullscreen mode Exit fullscreen mode

Just functions all the way down. We did the the thing, Re-frame style! I'll leave killing the goblins as an exercise for the reader.

Top comments (0)