Before Rails 3, the way to protect our Rails app against mass-assignment was to use the attr_accessible class method on our ActiveRecord models.
This was a less than ideal solution since our models, which represent a database table, had some knowledge of what kind of data our web servers were receiving.
The solution that the Rails community has found was the strong_parameters
gem. The goal of this gem is to filter params at the controller level. This is a better solution, but I find it hard to use to filter complex data structures.
Even this example found in the documentation is pretty hard to understand.
params.permit(:name,
{:emails => []},
:friends => [ :name,
{ :family => [ :name ],
:hobbies => [] }])
To understand the solution that I propose, you need to understand partial application of functions, which is one of the key features of some functional languages such Haskell, OCaml, Elm etc. Note that in this document when I talk about functions, I am talking about lambdas or procs.
Here is the definition of partial application taken from Wikipedia:
In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.
To better understand what is partial application, let’s start with an example.
Let say that you have the add
function that you define using lambda.
add = -> a, b { a + b }
You can call this function this way:
add.(1, 2)
#=> 3
But you can write it using partial application
add = -> a { -> b { a + b } }
Now you can call the function this way:
add_1 = add.(1)
# => #<Proc:0x007fcf57a58a60@(irb):12 (lambda)>
add_1.(2)
# => 3
This means that we can partially apply 1
to the add
function then apply 2
. Note that when we pass the last param, the result gets computed.
So you can call the same function this way:
add.(1).(2)
# => 3
This process of converting a function with multiple parameters to a function that recursively returns a function is called currying. Currying can be painful if done manually. Ruby has a solution for that: it’s the curry
method defined on Proc
objects.
So you can take a function with multiple params and convert it to a function with one param that returns a function with one param up until there are no parameters left.
Here’s an example:
add_3_numbers = -> a, b ,c { a + b + c }.curry
Then your function will be curried. You can then apply parameters one after the other.
add3_numbers.(1).(2).(3)
# => 6
This gives us an easy way to apply the parameters on that function in different contexts. This is a very powerful concept that allows you to “initialize” functions before running them.
Let’s say that we define a function filter_hash
, which takes a list of keys
to keep from the params hash.
filter_hash = -> keys, params {
keys.map { |key| [key, params[key]] }.to_h
}.curry
We can use this function to filter a params hash like this one:
params = {name: "Joe", age: "23", pwd: "hacked_password"}
filter_hash.([:name, :age]).(params)
# => {name: "Joe", age: "23"}
This is great, but if we have a more complicated params hash:
user_params = {name: "Joe", age: "23", pwd: "hacked_password",
contact: { address: "2342 St-Denis",
to_filter: ""}}
We might want to apply a filter to the contact hash. But the filter_hash
function does not allow us to apply a filter to the values of the params hash.
filter_hash.([:name, :age, :contact]).(params)
# => {name: "Joe", age: "23",
# contact: { address: "2342 St-Denis",
# to_filter: ""}}
Instead of using the filter_hash
function, we can define the hash_of
function, which takes two params. The first one is the fields
param, which is a hash that maps each key you want to keep to a function that will be applied to the corresponding value in the params hash. The second one is the hash
param, which corresponds to the hash be filtered.
Here is the definition:
hash_of = -> (fields, hash) {
hash ||= {} # The hash can be nil in the case of nested hash_of
fields.map { |(key, fn)| [key, fn.(hash[key])] }.to_h
}.curry
It’s easier to understand using an example.
user = hash_of.(name: -> a {a},
age: -> a {a},
contact: hash_of.(address: -> a {a}))
user.(user_params)
# => {name: "Joe", age: "23",
# contact: { address: "2342 St-Denis" }
Note that the -> a { a }
function is very useful in functional programming. It is so useful that it has a name. It is called the id
function. It takes a param and returns it as is. Since id
is used in a different context in Rails, we will rename it to same
.
same = -> a { a }
same.(2) # => 2
We can then rewrite the user
function
user = hash_of.(name: same,
age: same,
contact: hash_of.(address: same))
This very small function makes the filter functions more DSLish.
Note that these functions are very composable. We can put the contact
filter in a variable:
contact = hash_of.(address: same)
user = hash_of.(name: same, age: same,
contact: contact)
You can then reuse this filter in another controller very easily.
If we want to filter a field that contains an array. We can use the following function:
array_of = -> fn, value { value.kind_of?(Array) ? value.map(&fn) : [] }.curry
Let say that we have a list of contacts:
contacts_params = [{address: "21 Jump Street", remove: "me" },
{address: "24 Sussex", remove: "me too" }]
We can use our array_of
to filter out our contacts.
array_of.(contact).(contacts_params)
# => [{address: "21 Jump Street"},
# {address: "24 Sussex"}]
With strong_params
gem, it can be hard to set a default value on blank
data. But using these very simple functions, it gets very trivial. We can create a default
function.
default = -> default, a { a.blank? ? default : a }.curry
contact = hash_of.(address: default.("N/A"))
params = [{address: "21 Jump Street", remove: "me" },
{address: ""}]
array_of.(contact).(params)
# => [{address: "21 Jump Street"},
# {address: "N/A"}]
Using the same
function can cause some security issues. If you are expecting the value of a field to be a string and you get a hash, that function will return that hash. A solution to that problem is the scalar
function
scalar = -> a { a.kind_of?(Array) || a.kind_of?(Hash) ? nil : a }
Here’s an example
hash_of.(name: scalar).(name: {hack: "coucou"})
# => { name: nil }
hash_of.(name: scalar).(name: "Martin")
# => { name: "Martin" }
Here’s a way to rewrite the example that we’ve extracted from the strong_parameters
documentation.
The original one
params.permit(:name,
{:emails => []},
:friends => [ :name,
{ :family => [ :name ],
:hobbies => [] }])
The functional one
friend = hash_of.(name: scalar,
family: hash_of.(name: scalar),
hobbies: array_of.(scalar))
hash_of.({ name: scalar,
emails: array_of.(scalar),
friends: array_of.(friend)})
Ok, it’s a bit more code, but it is a way simpler solution, easier to understand, easier to test, more extensible, more reusable, and more composable. The only drawback is that you need to be familiar with partial application. However, knowing partial application can improve your code in different ways.
It is straightforward to extend this solution by creating your own functions. Nothing forbids you to create validations, cast functions, etc. If you want to extend the strong_parameters
gem you have to monkey patch it, which is pretty bad.
You can also use this pattern in other contexts, such as converting JSON structure coming from a remote API. Since you can set defaults or cast data like dates, it is a perfect solution to filter/transform complex JSON payloads structure.
With only a few very simple functions, you can do a good part of what the strong_parameters
gem does.
Here’s a recap of the functions that we’ve talked about in this blog post.
same = -> a { a }
hash_of = -> fields , hash {
hash ||= {}
fields.map { |(key, fn)| [key, fn.(hash[key])] }.to_h
}.curry
array_of = -> fn, value { value.kind_of?(Array) ? value.map(&fn) : [] }.curry
default = -> default, a { a.blank? ? default : a }.curry
scalar = -> a { a.kind_of?(Array) || a.kind_of?(Hash) ? nil : a }
This solution is so simple that bugs are very less likely to exist. It covers most functionalities that the strong_params
gem is offering, but I think that with minor changes, we would be able to support all of them.
In the next post I will show you how to integrate this solution in your Rails application.