Parser + Actions + Reducers. Lazy Jar — Part #1

Grace Elina
Artris Code
Published in
6 min readJun 16, 2018

This is part one of a series of articles on Lazy Jar. A slack app for scheduling remote stand-ups and tracking participation in Node.js. Fork the repo to start experimenting. If you’re new to this project? Start with the introduction.

The user can send commands to lazy-jar (the slack app) to schedule or terminate a standup, add or remove members, allow members to take a break, etc. Lazy-jar will only understand commands that follow a specific syntax and returns a meaningful error message otherwise. The commands read like a spoken sentence, but you can think about them as shell commands with lots of flags.

example response error messages sent back to user

Why our own parser?

The design of our commands was simple enough that we just jumped into implementing our own parser instead of using one already available. We did this because:

  1. We didn’t want to use other services. We wanted to keep everything in a single node to save costs. Go monolith!
  2. We still needed to write validators and reducers anyway so we thought we might as well write a parser too.

Keeping it simple … with one exception

While implementing the app, we followed the “keep it simple, stupid” principle to get a working, well organized, tested and straightforward code base. It got to a point where if anything was complicating our design, we just removed it. That’s how stupidly simple we kept it.

This simplicity allowed us to quickly create an MVP that focuses on returning meaningful error messages to nudge the user in the right direction. User friendliness was our one exception to our “simple” rule. We did not take shortcuts here. Why? Well, Max Howell the creator of Homebrew explains it better than I ever would.

On the other hand, my software was insanely successful. Why is that? Well the answer is not in the realm of computer science. I have always had a user-experience focus to my software. Homebrew cares about the user. When things go wrong with Homebrew it tries as hard as it can to tell you why, it searches GitHub for similar issues and points you to them. It cares about *you*. Most tools don’t give a shit about you. If they go wrong, well screw you. Homebrew helps you. And if it can’t help you I made it so, so easy to fix Homebrew (I built a command into the base for editing and fixing Homebrew). You can make Homebrew better. Homebrew is a shining example of true Open Source.

A big part of how we can provide useful feedback to the user is when the bot fails to interpret the commands — finding out exactly what went wrong.
To do this, we divided the process of interpreting a user command into three steps handled by the parser, actions, and reducers. The result produced at each step is then carried over to the next.

  1. Parser: transforms a textual command into an action (JSON representation) that is easily consumed by the other modules in the system
  2. Action: validates the JSON object returned by the parser and transforms its properties to create an action
  3. Reducer: given the current state of a team and a valid action, returns the next state

At each step of this process we return different error messages. In the parser we have syntactical errors while the actions give semantic errors. By the time we reach the reducers, we can create a new state and act on the user command.

Parser

In the parsing step, we extract the command type and any relevant details needed to later emit an action. For example, when the user sends /lj add @grace to 6amCrew, we extract the command ADD, the username @grace and the standup name 6amCrew and return a JSON object holding this information. No sort of validation happens here. Its a simple matter of — Yep, the user entered the correct syntax for us to gather enough information about the command. Or nope, try again.

The root parser is just a switch statement that passes a user command to a relavant parser.

switch statement that passes a command to a relevant parser
example of a parser function

Actions

The next step is to validate and transform the data and return an action. A json object returned from the parser might look like this:

During validation, we should be asking questions like — do the usernames actually exist? If we are making changes to a standup, does the standup exist? Did the user enter a time for a standup that we can understand? As we answer these questions we map each field to a format that the reducers can easily consume. For example a string specifying a period is mapped to a date or usernames are mapped to user Ids. This step is all about making sure data is valid and mapping this data into an action that can be passed to the reducer. The returned result can look something like this:

If during this step no errors are thrown and an action is returned we can move on to the reducers.

Reducer

The reducer takes in the current state and an action and returns a new state. Here nothing much is done except for making a new state based on the validated action data. The new state is then returned.

the reducer function

State

Once the reducer has returned a new state, we update the state in the database. The state only stores information about the current event configurations. The state of each event has a team and event id which uniquely identifies an event for a team. Other state attributes include:

time_to_respond
The time, in seconds for which a member can respond to a meeting notification and still be counted as present in the logs.

members
An array listing the users for an event and the corresponding identifiers for that user.

skip_until
A member on a break will have this attribute. It holds a date value specifying until when that member should not receive event notifications.

frequency
A keyword giving details of how often an event happens, for example, everyday, weekends, etc.

time
A JSON object representing the time in hours and minutes of the event and the timezone for which that time is valid.

halted
Specifies whether an event is ongoing (true) or presently halted (false).

The reason why we only store the current state in the database again resonates with the “keep it simple, stupid” principle. The current user configuration stored in the state gives us all the necessary information to schedule events and send users notifications.

Next

Next, we will explore the Node Schedule package and our wrapper scheduler around it.

Lazy Jar: Scheduling recurring events

--

--