Decisive

DSL for rendering and streaming CSVs in Rails apps

CI Status Code Climate

Usage

Example usage:

# app/controllers/users_controller.rb

class UsersController < ApplicationController
  include ActionController::Live # required to stream; decisive will fall back to rendering without it

  def index
    @users = User.all
  end
end
# app/views/users/index.csv.decisive

csv @users, filename: "users-#{Time.zone.now.strftime("%Y_%m_%d")}.csv" do
  column "Email" # omitted accessor field gets inferred: user.email
  column "Full name", :name # explicit accessor field: user.name
  column "Signed up" do |user| # accepts a block for doing something special
    user.created_at.to_date
  end
end

Then visit /users.csv to stream a file named "users-2010_01_01.csv" with the following contents:

Email Full name Signed up
[email protected] Frodo Baggins 2002-06-19
[email protected] Samwise Gamgee 2008-10-13

Non-streaming usage for non-deterministic headers:

Sometimes, we don't know exactly what the headers will be until we've iterated through every record.

For example, lets say that the Frodo record has a #faqs attribute of { "Riddles?" => "Yes" }, while Sam's is { "Hero?" => "Frodo" }.

In this case, you can pass stream: false to #csv, and the method will yield each record to the block:

# app/views/users/index.csv.decisive

csv @users, filename: "users-#{Time.zone.now.strftime("%Y_%m_%d")}.csv", stream: false do |user|
  column "Email"
  column "Full name"

  user.faqs.favorite_questions_and_answers.each do |question, answer|
    column question, answer
  end

  column "Signed up", user.created_at.to_date # we have access to the user record directly
end

Visiting /users.csv will render a file named "users-2010_01_01.csv" with the following contents:

Email Full name Riddles? Hero? Signed up
[email protected] Frodo Baggins Yes 2002-06-19
[email protected] Samwise Gamgee Frodo 2008-10-13

Debugging

Errors in your decisive template will often be swallowed while streaming is enabled, resulting in only some of the csv being rendered, without any explanation. You can temporarily switch decisive into non-streaming mode to see these errors:

# decisive template

csv @records, filename: "report.csv", stream: false do
  ...
end
# controller

class ReportController < ApplicationController
  # include ActionController::Live

  ...
end

Gotchas

Sometimes the streaming templates will hang in test mode. This could be because action_controller/test_case has been loaded, which monkeypatches AC::Live in a way that seems to breaks things. If you are running into this, try this workaround (cucumber example):

# features/support/fix_ac_live.rb
# requiring cucumber/rails requires rails/test_help which requires action_controller/test_case which redefines AC::Live#new_controller_thread to be single threaded, which breaks our downloads
# set it back to original method definition

module ActionController
  module Live
    silence_redefinition_of_method :new_controller_thread
    def new_controller_thread # :nodoc:
      Thread.new {
        t2 = Thread.current
        t2.abort_on_exception = true
        yield
      }
    end
  end
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/botandrose/decisive.

License

The gem is available as open source under the terms of the MIT License.