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.