Hanami 2.0 Ideas


#1

This is a list of ideas that I collected over the time, both while developing and using Hanami.

They are five different categories of intervention:

  • Add - This indicates new features
  • Improve - Enhance existing features
  • Fix - Repair existing features
  • Remove - Features to remove
  • Change - Breaking changes in features

Some ideas can be shipped with v1.1, as they can work alongside with current code. Some other will introduce breaking changes, or internal rework and we want to wait for 2.0.

Please consider these ideas as an input for the future, not as a roadmap. I want to validate them with the Community. You’re invited to give us advice for these ideas, or propose your own ideas.

:cherry_blossom:


#2

General Principles for 2.0

Before to dive into single features, I want to discuss about general principle to follow for 2.0.

Remove Global State

Global state is cause of major pain for Hanami development and at the same time prevented to ship some features.

For global state in the context of Hanami, I mean global configurations attached to class methods or constants. For instance: Hanami::Model.configuration.

This is the thing that I regret the most in Hanami architecture.


Let me explain why this is a problem both for us and for developers.

Within a Hanami project we are able to use one or more Hanami applications. Let’s name them Web and Admin. Each application has its own set of actions, views, etc…

All the actions belonging to Admin need to follow its configuration. For instance, the session secret should be the one from Admin, while the one from Web must not interfere.

Because hanami-controller is designed with global state in mind, it holds the configuration in Hanami::Controller.configuration. Now, if both Admin and Web try to read and write settings from there, then we’ll have a problem.

That’s why I invented a technique named framework duplication. Before to read or write settings duplicate the framework under a different namespace and copy the configuration. So Hanami::Controller.configuration gets copied under Admin::Controller.configuration and only now I can read/write settings.

That’s why we include Admin::Action, instead of Hanami::Action.

This sucks.

It’s source of confusion for new developers and a PITA for us to test Hanami::Controller.configuration both in hanami and hanami-controller.

That class method is a singleton, and singletons (by definition) prevent to work with multiple instances of a same class.


Wouldn’t be better if we can get rid of this insanity?

Imagine if behind the scenes we could do:

configuration = Hanami::Controller::Configuration.new do
  # ... configure here
end

We can store this configuration object somewhere (see next point) and read/write from it. This would eliminate at all Hanami::Controller.configuration, in favor of isolated configuration instances.

Also imagine if instead of having Hanami::Model.connection that targets one database, we could have multiple connections to target multiple databases.

Components

Luckily, as v0.9 we introduced a solution for this problem, but we didn’t fully applied it yet because it required changes in hanami-* gems too.

At the boot time, we store some configurations in a registry called Hanami::Components. The goal is to have all the configurations stored here.


What are the advantages of this registry?

First of all, it ensures components are stored once (at the boot time) and never changed. It’s thread-safe, and it’s able provide fine-grained control over components.

When we start the server (via hanami server), we need to load the entire project. But there are cases where this isn’t necessary: migrating a database doesn’t require to load the actions, precompiling the assets doesn’t require to connect to the database, etc…

With this system we are already able (as of v0.9) to pick single components where needed.

If we use this system for everything, we can say goodbye to framework duplication: no more Web::Action, Admin::Action, but just include Hanami::Action and let the components to pick the right configuration for a given action.

This will eliminate all the singletons Hanami::View.configuration et all, in favor of Hanami::Components['apps.web.view'] and Hanami::Components['apps.admin.view'].

IoC/Dependency Injection

We should favor dependency injection (DI) everywhere.

There are components that don’t play well with this technique: I’m looking at you, interactor.

To ease DI, we should change the signature of some methods. For instance:

class UserSignup
  include Hanami::Interactor
  
  def initialize(data)
    @data = data
  end
  
  def call
    # ...
  end
end

What’s the problem here?

If I want to use DI in an action unit test, I have hard times because the initializer needs some arguments that I’m not always able to provide at the init time. See:

module Web::Controllers::Signup
  class Create
    include Web::Action

    # This is impossible
    def initialize(interactor: UserSignup.new(params))
      @interactor = interactor
    end
    
    def call(params)
      @interactor.call
    end
  end
end

What we need to do is to have more callable objects.

Imagine this version of the interactor:

class UserSignup
  include Hanami::Interactor
    
  def call(data)
    # ...
  end
end

At this point we can easily use DI:

module Web::Controllers::Signup
  class Create
    include Web::Action

    def initialize(interactor: UserSignup.new)
      @interactor = interactor
    end
    
    def call(params)
      @interactor.call(params)
    end
  end
end

In this way, we can easily pass a double in the unit test:

interactor = double('signup', call: ...)
action = Web::Controllers::Signup::Create.new(interactor: interactor)

With DI in place, we can provide advanced features for dependencies management.

Think if we can have a convention to auto-register dependencies and to inject them.

class BookRepository < Hanami::Repository
  def on_sale_books
    books.where(on_sale: true)
  end
end

Then we could reference a repository like this:

module Web::Controllers::Books
  class Show
    expose :on_sale_books

    def call(params)
      @on_sale_books = books.on_sale_books
    end
  end
end

This is an idea inspired by dry-auto_inject.

Immutability

To allow these features to work properly, we need to make sure repositories, actions, views, won’t change their state. They must hold only the minimum state as possible.

Because of mutability, for each HTTP request we need to instantiate at least an action, and a view. If there are other objects involved (eg a repository) it will be instantiated too.

If we combine DI/IoC with immutability we can save a lot of memory allocations per request.

Imagine if at the runtime we can have the only instance of Web::Controllers::Books::Index registered as Hanami::Components['web.controllers.books.index']. If this instance is immutable we can keep invoking #call for each request, without instantiating it anymore.


This requires design changes for these objects. Action.expose must be removed in favor of this:

module Web::Controllers::Books
  class Show
    include Web::Action
    
    def call(req, res)
      res[:books] = BookRepository.new.find(req.params[:id])
    end
  end
end

Also Action#body=, Action#headers should be replaced with:

require 'json'

module Web::Controllers::Books
  class Index
    include Web::Action
    
    def call(req, res)
      books = ...
      res.headers['Content-Type'] = "application/json"
      res.body = JSON.dump(books)
    end
  end
end

In this case, it’s res that mutates, not the action itself.

Converge Apps Settings to Project Settings

If we look at the configuration of each single Hanami application in a project, there are a lot of settings.

These are overwhelming for newcomers and it doesn’t make sense to be duplicated across the project.

Logger is a good example. Until 1.0 we had one logger per app: Web.logger, Admin.logger. This had the drawback to maintain the relative settings and to operate a project with several loggers.

With 1.0, we converged all of them in Hanami.logger.

The settings are centralized into config/environment.rb instead of apps/web/application.rb and apps/admin/application.rb.

This means less overhead for Hanami and less maintenance for developers.


For the future we can think to unify:

  • routes (with a single router, instead of two tier system)
  • exception handling
  • scheme/host/port settings
  • Body parsers
  • Common Security features
  • Assets compressors
  • Assets subresource integrity

Be More Opinionated

There are settings that can be removed from application configurations, by providing an implicit default that can be changed at a developer’s convenience.

Think of the following settings:

module Web
  class Application < Hanami::Application
    configure do
      root __dir__
      routes 'config/routes'
    end
  end
end

Who ever changed root? Who ever placed routes under another path?

We’re creating noise for the 99% of the developers who don’t need these settings.

The goal is to reduce the boilerplate of these settings, not generating them for new apps.

So a new generated app will have less settings, that can be changed in case a developer needs them. Eg. load_paths can be removed by letting the framework to pick a default for us (controllers and views dirs). In case we need to customize it, we can type:

module Web
  class Application < Hanami::Application
    configure do
      # ...
      load_paths << 'presenters'
    end
  end
end

Extendibility

We want keeping encouraging third-party developers to write integration gems for Hanami.

One goal is to allow to register custom commands or subcommands:

hanami foo

or

hanami generate authentication user

Performance

We’re faster than Rails, but slower than Sinatra. Wouldn’t be nice if we can offer the best of the two worlds? A framework that is both complete and fast.

To do so we need to:

  • Have a single tier router. Right now we have Hanami.app that is a proxy router to dispatch requests to mounted apps. Each app has it’s own router. So for /admin/books request, we get the main router to dispatch to Web and Admin. Then Admin router dispatch the request to the internal route. This two tier system is slow.
  • Replace http_router with roda for hanami-router
  • Embrace immutability and components (see above). We can save a lot of instantiated objects per request. This is a relief for Ruby garbage collector and so for performance.
  • Profile existing projects to find bottlenecks, useless allocations etc…

#3

#4

Improve CLI commands

Our implementation is based on thor, which doesn’t work well with subcommands. Aside from that, the implementation of the generators is hard to maintain. Luckily we have a really good integration testing coverage so we can apply the following ideas.

Switch from thor to optparse

As mentioned above, thor doesn’t work well with subcommands and with exit codes. We can replace it with optparse from Ruby’s standard library.

thor ships with useful file manipulation utilities. We can promote our file helpers from our test suite as replacement.

Our helpers don’t provide a consistent indentation of Ruby code. We should find a solution for that (https://rubygems.org/gems/rubyfmt ?).

Commands as objects

thor demands that commands are methods of an object.

class MyCli < Thor
  def command_a
  end
end

We want to Implement each (sub)command as an object. This approach allows us to put args validations in each class and it decouples the command (hanami routes) from the concrete implementation that can be provided by us or a third party gem.

class MyCommand < Hanami::Command
  # arguments validations DSL

  def call(arguments)
    # do something
  end
end

Let commands to register themselves

class MyCommand < Hanami::Command
  # hanami routes
  register_as "routes"

  # or ...

  # hanami generate auth
  register_as "auth", subcommand_of: "generate"

  # ...
end

This opens an endless possibilities for our ecosystem.

Please note that a third party-gem can take over an entire (sub)command (eg. hanami-cool-orm can take over hanami generate model)

Commands dependencies

There are commands that don’t need the full blown Hanami project to do their job.
Think of hanami routes, it only needs the routes from the project, without connecting to the database.

As of 0.9, we already let some commands to pick that dependencies. The goal is to let all the commands to work with this approach. See this example: https://github.com/hanami/hanami/blob/master/lib/hanami/commands/routes.rb


These features can be shipped in the 1.x series, as they don’t introduce breaking changes for our users.


#5

Improve Code Reloading

We use shotgun as a soft-dependency for code reloading. If required in Gemfile, hanami server auto-detects it and the code is auto-reloaded after each incoming HTTP request.

We broke code reloading a few times, and the only way to fix it was to introduce Hanami::Utils.reload! in order to load again the files under lib/.

We can investigate alternatives for shotgun and integrate them:

  • guard my favorite, it comes with a lot of useful plugins like guard-livereload and it works on Windows too.
  • rerun it works out-of-the-box, with less configurations. It works only on *NIX.
  • entr this is a *NIX executable written in C. We introduced experimental support for it from 0.8 to 0.9 and didn’t worked well. But some people in our chat are promoting it, so we can have a second look.

The goal is still the same: keep Hanami project unaware of code reloading. Only hanami server is aware of this feature, and it acts as a reloadable shell for a project.


The target release for this has to be decided. There are some solutions like rerun and entr that don’t require changes in existing projects, while guard requires the addition of a Guardfile.

Depending on the decision that we’ll take and the effort it can be in 1.x series or even in 2.0.


#6

Native Webpack Support

I confessed several times, I’m a backend guy. Hanami assets as they are now, work fine for my needs. However, we received several feedbacks about how this system is limiting and how Webpack is better for advanced features. No doubt it is. So why don’t let frontent devs to work with their favorite tool?

By default, a new project will have the actual assets system (hanami new bookshelf).
But when the --webpack flag, it generates the configurations needed by Webpack.

There are a few technical issues to address:

  • How to hook Webpack paths for assets with our helpers (eg. javascript)?
  • We should provide a smooth development experience to start both Hanami and Webpack server. My idea is to use foreman and a Procfile.dev, and when hanami server is started, it will use it.

This will probably need to introduce breaking change, so the target release is 2.0. I want a frontend dev to work/advise on this.


#7

Unobtrusive JS (UJS)

The idea is to use vanilla-ujs gem to provide AJAX capabilities to Hanami, without depending on a JS framework.

This is a two steps improvement:

  • Bundle vanilla-ujs files to make them available into a Hanami project
  • Expand the capabilities of our helpers (eg. form_for, link_to).

This can theoretically be part of the 1.x series.


#8

New Router Implementation

hanami-router uses http_router as engine. This gem served well us until now, but it’s no longer maintained. We want to switch to roda .


This will require breaking changes in the routing API, so the target release is 2.0.


#9

Events/Instrumentation

Introduce an events system that can provide an API to emit/handle them. The system is similar to ActiveSupport::Notifications, and it should use wisper gem.

The idea is to “broadcast” an event happening in the framework or in a Hanami project and let the other part of the system to react to it. There are several scenarios.

Pub/Sub

class WelcomeMailer
  subscribe_to "user.signup"
end
class DripAdapter
  subscribe_to "user.signup"
end
class Signup
  include Hanami::Interactor

  def call(data)
    user = UserRepository.new.create(data[:user])
    broadcast("user.signup", user: user)
  end
end

When that event is emitted, WelcomeMailer and DripAdapter will receive a notification and react accordingly.

Async Execution

Continuing on the previous example, the broadcast is immediate, but the execution of the receiver logic can be async. We can send to a queue system (eg. sidekiq) the name of the event and the payload and let it to schedule the execution.

The API could be

broadcast("user.signup", user: user).async(...)

Instrumentation

def render
  broadcast('rendering.template') do
    # ...
  end
end

This reports the elapsed time. We can use it to log when a template was rendered.


This can be part of 1.x series.


#10

Move Mailers To apps/

Hanami architecture has one golden rule: the core of a Hanami project lives in lib/, while the deliverability mechanisms (for the web) live in apps/. The code in apps/ can reference (depend) code in lib/ but NOT viceversa.

Because we offer Interactors (aka service objects) as part of the core of a Hanami project, their natural place is lib/.

Interactors sometimes need to send emails, so to not break the golden rule above, we put mailers in lib/ too.

This is wrong for two reasons:

  • Mailers are deliverability mechanisms too, so they shouldn’t stay in lib/
  • If they stay out of apps/ they cannot access to assets helpers

The solution is to move mailers to apps/, and so the command to generate a mailer should go from:

hanami generate mailer welcome

to:

hanami generate mailer web welcome

Where web is one of the applications living under apps/. This is the same concept of hanami generate action command.


Because this is a breaking change the target release is 2.0.


#11

I think we can ask help from Samuel Simões (https://github.com/samuelsimoes) for this


#12

Great, do you know Samuel?


#13

@jodosha Do you think it would be useful to have something like the route middlewares from Laravel or it doesn’t match with the framework?

Example

group admin: AdminMiddleware do
  resources 'books'
end

And have an AdminMiddleware called before every route on the group.


#14

Nope, but I think it will not a problem for us :slight_smile:
We can send an email or tweet him.


#15

What’s the advantage of this? Do you know you can add a middleware in front of single apps as now? Thanks.


#16

Ok, but why him then?


#17

because he is the author of https://github.com/samuelsimoes/hanami-webpack and I think he have experience in this question


#18

I didn’t know that, can you give me some pointers please?


#19

@CassioGodinho please open apps/web/application.rb and search for middleware.use, it’s commented code.


#20

One advantage I can think of is having a Middleware for only some routes on an app that needs to be authenticated and halting the request before it gets to the controller eliminating the need of a before :authenticate!.

Maybe I’m just trying to find a way to work the way I’m used to work on Laravel and I have yet to understand how things work with Hanami.