Iterate over indexed params in Rails

When your API isn't Rails-shaped, sometimes you need to handle input differently

When building out an API, sometimes the input you need to receive isn’t always in the format that Rails would necessarily like.

Occasionally it is worth the effort to force it into a shape to let Rails magic happen, but more often than not the best thing to do is work with what you have.

In a recent project we had an API request that wanted to create multiple things (we will call them Widgets) in one request.

The input looked something like this;

widget_0_name: 'New Widget'
widget_0_description: 'More details here'
widget_1_name: 'Another Widget'
widget_2_name: 'And one more for luck'
widget_2_description: 'Final description'

This is close but not exactly how Rails would normally deal with a form with nested attributes for another model.

What we want to be able to do is create multiple Widgets based on this above input.

In our controller, what we ended up doing was something like;

def create
  # ... other create tasks
  Widget.create(widget_params) if any_widgets?
end

private

def any_widgets?
  params[:widget_0_title].present?
end

def widget_params
  count = 0
  widgets = []
  while params["widget#{count}_title"].present?
    widgets << {
      title: params["widget_#{count}_title"],
      description: params["widget_#{count}_description"],
    }
    count += 1
  end
  widgets
end

I will go through the important lines in the above code.

Widget.create(widget_params) if any_widgets?

What we are saying here is “Create some Widgets, if we have any to create”. Both widget_params and any_widgets? are methods we are going to make.

def any_widgets?
  params[:widget_0_title].present?
end

This method is a helper that checks for the existence of one param. It is a little bit of a hack because it isn’t really looking for “any” widget, but considering we can document that all widgets need to be zero indexed and the alternative would be a much more complex and slower check.

In our set up, “title” was the only thing we cared about, we could add all required params here.

def widget_params
  count = 0
  widgets = []
  while params["widget_#{count}_title"].present?
    widgets << {
      title: params["widget_#{count}_title"],
      description: params["widget_#{count}_description"],
    }
    count += 1
  end
  widgets
end

The widget_params method loops through our required param, incrementing the counter by one each time, until it runs out of valid input.

widgets is set to an empty array and the while loop shovels in a hash of allowed parameters.

At the end we return our widgets array.

What I like about this

There is very little magic here, I think each method does what it claims to do, and a developer with any amount of experience should hopefully be able to follow this through.

What I don’t like about this

This isn’t watertight. If the API were to send through something with an index of 0 then an index of 2 (skipping and index of 1) then we would fail to handle the second Widget.

I also don’t love that we’re replicating code that Rails normally gives us for free. This could get quite long with a more complex object, and I think if the object were lots more complex, I’d refactor this into something else.


Recent posts View all

Web Dev

Creating draft posts in Jekyll

How to create and develop with draft posts in Jekyll

Ruby

Forcing a Rails database column to be not null

How you can force a table column to always have something in it with Rails