Loading...
X

Strong-Like-Bull – A Lesson in Recursion

&TL;DR;

I’ve created a new gem called StrongLikeBull that suggests what parameters to permit in your strong parameters configuration based on what request parameters a controller action receives.

After repeating the same task multiple times in a row, I start looking for shortcuts. The problem I was facing was how to configure Strong Parameters in our applications as part of a company-wide effort to upgrade to Ruby on Rails 4.2. There were a lot of controllers in the application I was configuring, and this quickly became a very tedious task. For each controller, I was configuring debugger lines in the controller action, examining the request params, and then manually figuring out how what the strong parameters format would look like. Then, given the strong parameter’s required field and permitted params, I could run the strong parameters expression against the request params and make sure I was getting the same output as input.

So after repeating this several times, I began to wonder if there was a better way. A Google search revealed nothing (perhaps I couldn’t figure out what exactly to search for). I also came across a more complex example: some of our api controllers get hit from external services, and we don’t necessarily know what params they are passing. Certainly some auditing was in order, but this also represented another strong case for the use of strong params. Finding no viable solution out there, I began to explore implementing my own.

Because request params can be passed in the form of hashes, arrays, hashes of arrays, and arrays of hashes, and they can be infinitely nested, this seemed like the perfect case for recursion. Recursion is usually a fun case study, so I figured I’d break down how I got Rails to automatically make suggestions of permitted params to configure for strong parameters.

OVERVIEW: DIVIDE & CONQUER

Given a request to a Rails application, such as to a sessions controller to login to a application, the request parameters will likely be very simple. They might look somethings like this:

With a basic understanding of Strong Parameters, it’s rather trivial to eye these parameters and manually convert them to Strong Parameters format:

What happens though, when your example is thoroughly more complex than this? What happens when you’ve got nested attributes, and they have nested attributes, and the nested attributes might have different fields being specified. How do you dissect a request that looks like this:

Into this:

Perhaps you’ve had to deal with this kind of example, or maybe a more difficult case. If so, you know the pain and tedium of meticulously converting over request params.

Instead, let’s try and solve this problem using a divide and conquer strategy. Let’s pull out chunks of the params hash and work on them individually and then combine the results.

STEP 1: IDENTIFY THE BASE CASES

Before we get into nested hashes, or arrays of hashes, or hashes with arrays as values, let’s consider two of the most basic ways params might get passed to your controller: hashes and arrays.

HASHES:

A login request might receive the following request params:

The strong parameters configuration for this request is super simple:

Notice what happened there? The object {email: “test@test.com”, password: “password”} changed from a hash to an array, with the elements of the array matching the keys of the hash: [:email, :password]. We’ll use this principle as one of the basic cases to handle.

If, then, we are examining a hash, each key of the hash that maps to a non-hash, non-array, will be represented as a symbol in the array. So {a1: 1, a2: “2”, a3: 2.3} becomes [:a1, :a2, :a3].

The code for such a block might look something like this:

ARRAYS:

A update request to a Post class might receive the following request params:

The strong parameters configuration for this request is likewise simple:

The piece I want to focus on is that the array of Strings got represented in strong parameter’s world as an empty array: []

This means that if we encounter an array where the elements of the array are non-hashes, non-arrays, we can simply treat this in strong parameters as a []

The code for such a block might look something like this:

Step 2: Identify the Recursion Cases

Now that we have the boundary cases considered, let’s examine how we would handle the non-basic cases: hashes of arrays, arrays of hashes, and hashes of hashes. This is where we will start to see the need for recursion.

HASHES OF ARRAYS

We actually already saw a recursion example above when we looked at the array example of updating a Post with the request params:

In the boundary cases, when we encountered a hash, we simply added the hash key to an array of permitted params. Now, however, we’re dealing with a hash element whose value element is an array. We already saw above how to deal with simple arrays (by returning an empty array []). So we know if we call our recursivesuggestedstrongparametersformat method with [“code”, “theology”], the function will return []. What we need then is for that return value to be set as the value of a new hash. Our expected format would then be: {tags: []} So how do we get there? Let’s update our function so it recursively calls itself:

ARRAYS OF HASHES

Next let’s consider arrays of Hashes, such as the following request, this time, setting the products for a particular object

Here, we’ve got an array of products, which are each represented by a hash. Our code above is not yet covering this case, but the expected output would be:

So how do we get there then? By combining cases we’ve already handled. We already know how to handle a hash with scalar values. We just saw how to handle a hash with an array as the value. So let’s sub in our missing piece in the code:

HASH OF HASHES

By handling previous cases, our code is already setup to handle a hash of hashes of hashes of etc. Let’s try it out:

STEP 3: HANDLE THE EDGE CASES

I wish I could say we were done there, and the fact of the matter is, this will likely handle most cases out there. But what about Rail’saccepts_nested_attributes_for method and how that passes params? Or how about an array of hashes, where some hashes might include extra keys.

If we run those cases against out code now, we’ll get incorrect results:

ACCEPTS_NESTED_ATTRIBUTES_FOR METHOD OF PASSING PARAMS

If you’re model accepts nested attributes for a has many relationship, then it passes parameters to your controller action slightly differently. The request parameters might look something more like this:

In computing our strong parameters format, we want to completely ignore the IDs “1” and “2” here. To do so, we should examine the first key in a hash and see if it’s an integer. If it is, we should only use the value of the hash and not the key. Our updated code would now look like this:

What we need is for the array of hashes above to essentially be combined into a single hash that includes all the keys:

With this combined hash, we already have the code necessary to handle it. So how do we combine hashes? At first thought, you might be tempted to say Hash#merge. And this would certainly handle the above case. But it would not handle the following case:

We have deep nestings here, so we need to use Hash#deep_merge (a Hash extension provided by Rails. If we deep_merge the two hashes above, we would get:

We also have to account for request parameters in the format:

We can handle both of these cases with a few minor tweaks to our function:

CONCLUSION

There’s really not much else to include other than the fact that recursion can be pretty awesome and can be used to solve a wide-variety of problems fairly succinctly. Obviously, there are refactorings that can be done to the code but I’ll leave that for another day. If there is enough interest in this article, I’ll write another one about how to use recursion to write your own ActiveRecord extension to generate the SQL insert statement for any record or set of records.

Leave Your Observation

Your email address will not be published. Required fields are marked *