CSV on Ruby on Rails
I’m trying to accept CSV formatted batches on a RoR controller like a normal params, without having to parse it, without extra controller code. I wanted that magic feeling one feels with JSON posts but using CSV.
I found that it was not as hard, the main issue was the lack of documentation
about the methods that need to be used, because Rails team loves :nodoc:
. So
I will share some code I used to make it work.
Parameter parser
The first thing we need is a custom parameter parser, that is an under-documented feature on Rails, to use it we just need to make an initializer:
# config/initializers/csv_param_parser.rb
require 'csv'
ActionDispatch::Request.parameter_parsers.merge!(
csv: -> (raw_post) {
parsed_batch = CSV.new(raw_post, headers: true, header_converters: :symbol)
{ batch: parsed_batch.map(&:to_h) }
}
)
You might need to fine-tune the options, but headers are required to have a hash instead of an array. Also your use case might require enabling a converter (to make it possible to have numbers or booleans).
Anyway, with that in place, you can write your controller to receive a CSV batch without any mention to CSV.
def import
batch = params.require(:batch).permit(:name, :value)
results = Result.create batch
redirect_to :index, notice: "Good, #{ results.size } imported"
end
And it should work.
Also, if json is to be accepted, you can use a JSON with the same values and it will just work
{ "batch": [{"name": "foo", "value": 12}] }
But have in mind that for this to work, the content-type
header must be set
correctly.
Testing CSV requests
For testing those endpoints, we will need to set the request body correctly and also the content-type on the request. Luckily Rails also have an undocumented method for this, that need to be run on the test or spec file:
RSpec.describe "BatchEndpoints", type: :request do
register_encoder :csv, param_encoder: -> params { params.to_s }
it 'create results with a csv body' do
post import_path, as: :csv, params: sample_csv
expect(response).to be_successful
end
end
The register_encoder method will make the as: :csv
do some magic, like
setting the content type. It will also try to encode the given params hash as a
CSV, but it will fail because the to_csv
method is not defined, so we will
need to define the param_encoder
block to handle this. Here I do nothing
because I already have a CSV I want to test. It is under-documented at the end
of a big block of documentation for integration tests.