ruby-on-railsruby-on-rails-4controllerrespond-toservice-object

respond_to and service objects


I'm currently using the docx_replace gem to automate the insertion of data into a set of documents. The gem is pretty straightforward; basically it runs in a special method within your rails controller like so (quoting from the documentation):

def user_report
  @user = User.find(params[:user_id])

  respond_to do |format|
    format.docx do
      # Initialize DocxReplace with your template
      doc = DocxReplace::Doc.new("#{Rails.root}/lib/docx_templates/my_template.docx", "#{Rails.root}/tmp")

      # Replace some variables. $var$ convention is used here, but not required.
      doc.replace("$first_name$", @user.first_name)
      doc.replace("$last_name$", @user.last_name)
      doc.replace("$user_bio$", @user.bio)

      # Write the document back to a temporary file
      tmp_file = Tempfile.new('word_tempate', "#{Rails.root}/tmp")
      doc.commit(tmp_file.path)

      # Respond to the request by sending the temp file
      send_file tmp_file.path, filename: "user_#{@user.id}_report.docx", disposition: 'attachment'
    end
  end
end

This has, however, bloated my controller so I've attempted to put this into a service object like so (continuing with the example above):

class UserReportService
    def initialize(user)
        @user=user
    end

    def user_report_generate
        respond_to do |format|
            format.docx do
                # Initialize DocxReplace with your template
                doc = DocxReplace::Doc.new("#{Rails.root}/lib/docx_templates/my_template.docx", "#{Rails.root}/tmp")

                # Replace some variables. $var$ convention is used here, but not required.
                doc.replace("$first_name$", @user.first_name)
                doc.replace("$last_name$", @user.last_name)
                doc.replace("$user_bio$", @user.bio)

                # Write the document back to a temporary file
                tmp_file = Tempfile.new('word_tempate', "#{Rails.root}/tmp")
                doc.commit(tmp_file.path)

                # Respond to the request by sending the temp file
                send_file tmp_file.path, filename: "user_#{@user.id}_report.docx", disposition: 'attachment'
            end
        end
    end
end

And have done the following within my controller:

def user_report
  UserReportService.new(@user).user_report_generate
end

However when I call the controller method, I get the following error:

17:58:10 web.1  | NoMethodError (undefined method `respond_to' for #<UserReportService:0x000000041e5ab0>):
17:58:10 web.1  |   app/services/user_report_service.rb:17:in `user_report_generate'
17:58:10 web.1  |   app/controllers/user_controller.rb:77:in `user_report'

I read up on respond_to and, if I'm understanding the documentation correctly, it's a method specific to the controller (this would explain the problem). How might I be able to get around this?


Solution

  • respond_to and send_file should remain in your controller, but the rest of the logic can be moved into the service object.

    First, make the service object return the temp_file:

    class UserReportService
      def initialize(user)
        @user=user
      end
    
      def user_report_generate
        # Initialize DocxReplace with your template
        doc = DocxReplace::Doc.new("#{Rails.root}/lib/docx_templates/my_template.docx", "#{Rails.root}/tmp")
    
        # Replace some variables. $var$ convention is used here, but not required.
        doc.replace("$first_name$", @user.first_name)
        doc.replace("$last_name$", @user.last_name)
        doc.replace("$user_bio$", @user.bio)
    
        # Write the document back to a temporary file
        tmp_file = Tempfile.new('word_tempate', "#{Rails.root}/tmp")
        doc.commit(tmp_file.path)
    
        # Return the tmp_file
        tmp_file
      end
    end
    

    Instantiate your service object, retrieve the temp file, and send it to the user:

    def user_report
      respond_to do |format|
        format.docx do
          tmp_file = UserReportService.new(@user).user_report_generate
          send_file tmp_file.path, filename: "user_#{@user.id}_report.docx", disposition: 'attachment'
        end
      end
    end