Hanami 1.3 > Hanami 2.2 : tips & notes about my journey

Amazing, glad to hear that!

Use case #10: streaming a file like unsafe_send_file in Hanami 1

Even if most files are served directly by the web server or your object storage, in some cases you need to stream a file in an action. Could be because it’s generated on the fly, or it is a private file, or any other reason.

Hanami 1 had a quite convenient unsafe_send_file method to do that. The “unsafe” part is because you could refer any file outside your working dir IIRC.

:warning: The solution below works but there is a simpler - and native - way of doing it, thanks to @afomera for pointing this out. I’m keeping it for the sake of posterity, but check the next message for a better solution :warning:

Hanami 2 does not provide an official way of doing it. After a few tries and a bit of help of my half-drunk coding buddy ChatGPT, I came up with this solution, which I included in a method of my base Action, for a feeling of retro-compatibility (which is not true because the arguments changed)

# auto_register: false
# frozen_string_literal: true

require "hanami/action"

module MyApp
  class Action < Hanami::Action

    def unsafe_send_file(request, response, file_path)
      if ::File.exist?(file_path)
        rack_file = ::Rack::File.new(nil)
        status, headers, body = rack_file.serving(::Rack::Request.new(request.env), file_path)
        response.status = status
        headers.each do |header_name, value|
          response.headers[header_name] = value
        end
        response.body = body
      else
        response.status = 404
        response.body   = "File not found"
      end
    end

  end
end

Let me know what you think if you have any other cleaner technique.

For information, Adam Lassek mentioned this on Discord, but I need to go live quite soon without 2.3 beta

The 2.3 beta includes support for Rack 3. My understanding is that responding with an Enumerable is all that’s required? I have not done that yet though

Thanks for reading, see you next time

Curious if hanami-controller/lib/hanami/action/response.rb at main · hanami/hanami-controller · GitHub worked for you or if the same problem existed?

Thanks for your feedback! Do you mean did I try reproducing the same behavior to see if it works? I must admit that I did not try it, and reading the source code of v1 makes me think that the solution would be more verbose…

that’s ok too! I just noticed the same method exited in 2.x but haven’t used it myself yet.

Oh my! Your link wasn’t referring to the v1, my bad!!

You’re totally right, it works with response.unsafe_send_file(file_path)
I tried naively, but without the response, which doesn’t make any sense in Hanami 2 context.

Thank you very much, I’ll amend this thread with a new proper way.

Lesson learnt, check the source code first :grinning_face_with_smiling_eyes:

Use case #10: streaming a file like unsafe_send_file in Hanami 1

After a convoluted attempt to imitate it in Hanami 2, it appears that simply using the same method unsafe_send_file, but on the response object, works pretty well.

For example:

module MySlice
  module Actions
    module Home
      class Index < MySlice::Action
        def handle(request, response)
          response.unsafe_send_file("/tmp/your_file.xlsx")
        end
      end
    end
  end
end

Be careful that if you use a relative path, it is relative to the file of the action you’re writing, not to the project root.

There is a reason why this API moved, which may help you to remember in the future: Actions in Hanami 2 are immutable, so anything that involved mutation in 1.x moved out of Action.

Defining responses used to be a mutation of the Action context:

# apps/web/controllers/dashboard/index.rb
module Web
  module Controllers
    module Dashboard
      class Index
        include Web::Action

        def call(params)
          self.status = 201
          self.body   = 'Your resource has been created'
          self.headers.merge!({ 'X-Custom' => 'OK' })
        end
      end
    end
  end
end

Since Actions are now immutable, those operations needed to move to an ephemeral response object.

2 Likes

You’re absolutely right, and I even knew about Actions immutability. That’s why I felt so dumb realizing that I even tried to use the function directly in the action :face_with_peeking_eye: Response was obviously the way to go.

Thank you for your feedback!

Yeah I know the feeling well, but in your defense it’s not like this is documented… at all :grimacing:

Use case #11: Sending emails

I got to the point where I needed to send transactional emails with my Hanami app, I wanted to render them with templates and that they would be easy to send, and ideally integrated into the framework sot that I could come from an action, a job, a rake task…

After a few tries I found that the hanami-mailer was a good choice to start, despite its rough edges that I hope I will be have the level to polish some day. Thanks to this episode from Hanami Mastery I had the instructions on how to integrate it so I adapted it into my app.

The instructions below are heavily inspired from what I learnt from the above article, all credit is due to @swilgosz , thanks a lot to him! :cherry_blossom:

First you’ll need to add the gem to your gemfile and call bundle install

# Gemfile
gem "hanami-mailer", github: "hanami/mailer", branch: "main"

Then you need to load what you need in a provider. I decided to use one provider per slice to load my templates aka mailers. I felt that it suits well with the fact that each slice will have its own way to create and send emails.

# slices/my_slice/config/providers/mailer.rb

MySlice::Slice.register_provider(:mailer, namespace: true) do
  prepare do
    require "hanami/mailer"
    
    configuration = Hanami::Mailer::Configuration.new do |config|
      config.root = target.root.join("mailers")
      config.default_charset = "UTF-8"
          
      if ENV['HANAMI_ENV'] != "production"
        config.delivery_method = :logger, {logger: Logger.new(STDOUT)}
        # FYI config.delivery_method = :test  is another possible option
      else
        # here you can specify more complex options for your SMTP params
        config.delivery_method = :smtp, {:enable_starttls_auto => false}
      end
    end

    register "configuration", configuration
  end

  start do
    configuration = target['mailer.configuration']
    # Hanami::Mailer requires to initialize all mailers before finalizing
    # config, and finalizing config before using them.
    mailers = Dir[configuration.root.join('*.rb')]
    mailers.each do |path|
      mailer_name = File.basename(path, '.*')
      target["mailers.#{mailer_name}"]
    end

    Hanami::Mailer.finalize(configuration)

    register "mailer", true
  end
end

=> With that config, all “mailers” found in slices/my_slice/mailers directory will be loaded and ready to be used from dependency container.
A mailer is a ruby class inheriting from Hanami::Mailer class

Here is a sample yet realistic mailer:

# slices/my_slice/mailers/notify_print_is_ready.rb

require "hanami/mailer"

module MySlice
  module Mailers
    class NotifyPrintIsReady < Hanami::Mailer
      
      include Deps['mailer.configuration']

      from "Support team <support@acme.com>"
      to ->(locals) { locals.fetch(:to_email) }
      
      subject ->(locals) { "Your print '#{locals.fetch(:project_name)}' is ready ✅"  } 
      template 'notify_print_is_ready'

    end
  end
end

This mailer will need an associated template, that will be rendered before sending the email.

# slices/my_slice/mailers/notify_print_is_ready.html.erb

<html>
  <body>
    <p>Your print <%= locals.fetch(:project_name) %> is ready</p>
  <body>
</html>

The email can then be sent from anywhere in your code:

MySlice::Slice["mailers.notify_print_is_ready"].deliver({
  to_email:       "jim.halpert@gmail.com",      
  project_name:   chapter_print.name,
})

And voilà :tada:

Unfortunately it does not seem possible to inherit from a base layout, I’d love to help on that some day, let me know if you have any pointer or any ongoing work about the topic.

Thanks for reading, see you next time

A small update about this thread: our first project running full Hanami v2 is now live since the beginning of november (you can check it on https://catalogue-studio.com )

I’ll keep adding bits of info here because our migration is not finished yet, many other parts of our app should move to v2 in the future.

Cheers

3 Likes