A Handy Look at Hanami

Jason Charnes

@jmcharnes | Lensrentals.com

Other Ruby Frameworks

  • Sinatra
  • Padrino
  • Cuba
  • Volt
  • A few others

https://blog.engineyard.com/2015/life-beyond-rails-brief-look-alternate-web-frameworks-ruby

What's Missing?

Hanami

"Hanami is a Ruby MVC web framework comprised of many micro-libraries. It has a simple, stable API, a minimal DSL, and prioritizes the use of plain objects over magical, over-complicated classes with too much responsibility."

Luca Guidi

Full-featured

Lightweight

Long-term

Testable

"I warn you that whether you're a total beginner or an experienced developer this learning process can be hard. After 10 years you develop a way of working, and it can be painful for you to change. However, without change, there is no challenge."

$ gem install hanami

Successfully installed hanami-utils-0.8.0
Successfully installed dry-configurable-0.1.7
Successfully installed dry-container-0.5.0
Successfully installed dry-equalizer-0.2.0
Successfully installed dry-logic-0.3.0
Successfully installed inflecto-0.0.2
Successfully installed dry-monads-0.1.1
Successfully installed ice_nine-0.11.2
Successfully installed dry-types-0.8.1
Successfully installed dry-validation-0.9.5
Successfully installed hanami-validations-0.6.0
Successfully installed url_mount-0.2.1
Successfully installed http_router-0.11.2
Successfully installed hanami-router-0.7.0
Successfully installed hanami-controller-0.7.0
Successfully installed hanami-view-0.7.0
Successfully installed hanami-helpers-0.4.0
Successfully installed hanami-mailer-0.3.0
Successfully installed hanami-assets-0.3.0
Successfully installed hanami-0.8.0
20 gems installed
$ gem install hanami

Successfully installed hanami-utils-0.8.0
Successfully installed dry-configurable-0.1.7
Successfully installed dry-container-0.5.0
Successfully installed dry-equalizer-0.2.0
Successfully installed dry-logic-0.3.0
Successfully installed inflecto-0.0.2
Successfully installed dry-monads-0.1.1
Successfully installed ice_nine-0.11.2
Successfully installed dry-types-0.8.1
Successfully installed dry-validation-0.9.5
Successfully installed hanami-validations-0.6.0
Successfully installed url_mount-0.2.1
Successfully installed http_router-0.11.2
Successfully installed hanami-router-0.7.0
Successfully installed hanami-controller-0.7.0
Successfully installed hanami-view-0.7.0
Successfully installed hanami-helpers-0.4.0
Successfully installed hanami-mailer-0.3.0
Successfully installed hanami-assets-0.3.0
Successfully installed hanami-0.8.0
20 gems installed
$ hanami new bookshelf

create  .hanamirc
create  .env.development
create  .env.test
create  Gemfile
create  config.ru
create  config/environment.rb
create  lib/bookshelf.rb
create  public/.gitkeep
create  config/initializers/.gitkeep
create  lib/bookshelf/entities/.gitkeep
create  lib/bookshelf/repositories/.gitkeep
create  lib/bookshelf/mailers/.gitkeep
create  lib/bookshelf/mailers/templates/.gitkeep
create  spec/bookshelf/entities/.gitkeep
create  spec/bookshelf/repositories/.gitkeep
create  spec/bookshelf/mailers/.gitkeep
create  spec/support/.gitkeep
create  db/migrations/.gitkeep
create  Rakefile
create  spec/spec_helper.rb

create  spec/features_helper.rb
create  db/schema.sql
create  .gitignore
run  git init . from "."
create  apps/web/application.rb
create  apps/web/config/routes.rb
create  apps/web/views/application_layout.rb
create  apps/web/templates/application.html.erb
create  apps/web/assets/favicon.ico
create  apps/web/controllers/.gitkeep
create  apps/web/assets/images/.gitkeep
create  apps/web/assets/javascripts/.gitkeep
create  apps/web/assets/stylesheets/.gitkeep
create  spec/web/features/.gitkeep
create  spec/web/controllers/.gitkeep
create  spec/web/views/.gitkeep
insert  config/environment.rb
insert  config/environment.rb
append  .env.development
append  .env.test

├── Gemfile
├── Rakefile
├── apps/
├── config/
├── config.ru
├── db/
├── lib/
└── spec
$ bundle install
$ bundle exec hanami server
# spec/web/features/visit_home_spec.rb
require 'features_helper'

describe 'Visit home' do
  it 'is successful' do
    visit '/'

    page.body.must_include('Bookshelf')
  end
end
$ bundle exec rake test

Run options: --seed 46642

# Running:

F

Finished in 0.014917s, 67.0398 runs/s, 134.0796 assertions/s.
# apps/web/config/routes.rb
root to: 'home#index'
# apps/web/controllers/home/index.rb
module Web::Controllers::Home
  class Index
    include Web::Action

    def call(params)
    end
  end
end
# apps/web/views/home/index.rb
module Web::Views::Home
  class Index
    include Web::View
  end
end
# apps/web/templates/home/index.html.erb
<h1>Bookshelf</h1>
# spec/web/features/visit_home_spec.rb
require 'features_helper'

describe 'Visit home' do
  it 'is successful' do
    visit '/'

    page.body.must_include('Bookshelf')
  end
end
  • Route
  • Controller
  • View
  • Template
$ bundle exec rake test
Run options: --seed 28944

# Running:

.

Finished in 0.010793s, 92.6533 runs/s, 185.3067 assertions/s.

1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
$ hanami generate model book
create  lib/bookshelf/entities/book.rb
create  lib/bookshelf/repositories/book_repository.rb
create  spec/bookshelf/entities/book_spec.rb
create  spec/bookshelf/repositories/book_repository_spec.rb

Domain Logic

Persistance Layer

Entities & Repositories

Entities

"An entity is something really close to a plain Ruby object, it doesn't know anything about our database structure. We should focus on the behaviors that we want from it and only then, how to save it."

# lib/bookshelf/entities/book.rb
class Book
  include Hanami::Entity
  attributes :title, :author
end
# spec/bookshelf/entities/book_spec.rb
require 'spec_helper'

describe Book do
  it 'can be initialised with attributes' do
    book = Book.new(title: 'Refactoring')
    book.title.must_equal 'Refactoring'
  end
end
$ bundle exec rake test
Run options: --seed 3092

# Running:

..

Finished in 0.013320s, 150.1501 runs/s, 225.2251 assertions/s.

2 runs, 3 assertions, 0 failures, 0 errors, 0 skips
$ hanami db create
$ hanami generate migration create_books
# db/migrations/2016..._create_books.rb
Hanami::Model.migration do
  change do
    create_table :books do
      primary_key :id
      column :title,      String,   null: false
      column :author,     String,   null: false
    end
  end
end
$ hanami db migrate
# lib/bookshelf.rb
# ...
mapping do
  collection :books do
    entity     Book
    repository BookRepository

    attribute :id,         Integer
    attribute :title,      String
    attribute :author,     String
  end
end
$ hanami console

>> BookRepository.all
=> []

>> book = Book.new(title: 'TDD', author: 'Kent Beck')
=> #<Book:0x007f9af1d4b028 @title="TDD", @author="Kent Beck">

>> BookRepository.create(book)
=> #<Book:0x007f9af1d13ec0 @title="TDD", @author="Kent Beck" @id=1>

>> BookRepository.find(1)
=> #<Book:0x007f9af1d13ec0 @title="TDD", @author="Kent Beck" @id=1>
# apps/web/templates/index.html.erb
<% if books.any? %>
  <% books.each do |book| %>
    <div class="book">
      <h2><%= book.title %></h2>
      <p><%= book.author %></p>
    </div>
  <% end %>
<% else %>
  <p class="placeholder">There are no books yet.</p>
<% end %>
# apps/web/controllers/home/index.rb
module Web::Controllers::Home
  class Index
    include Web::Action

    expose :books

    def call(params)
      @books = BookRepository.all
    end
  end
end
$ hanami generate action web books#new
# apps/web/config/routes.rb
get '/books/new', to: 'books#new'
# apps/web/templates/books/new.html.erb
<h2>Add book</h2>

<%=
  form_for :book, '/books' do
    div class: 'input' do
      label      :title
      text_field :title
    end

    div class: 'input' do
      label      :author
      text_field :author
    end

    div class: 'controls' do
      submit 'Create Book'
    end
  end
%>
$ hanami generate action web books#create --method=post
# apps/web/config/routes.rb
post '/books', to: 'books#create'
# apps/web/config/routes.rb
post '/books', to: 'books#create'
# apps/web/controllers/books/create.rb
module Web::Controllers::Books
  class Create
    include Web::Action

    expose :book

    def call(params)
      @book = BookRepository.create(Book.new(params[:book]))
      redirect_to '/'
    end
  end
end
# apps/web/controllers/home/...
# apps/web/views/home/...
# apps/web/templates/home/...
# lib/bookshelf/entities/...
# lib/bookshelf/repositories/...

Full Featured?

  • check_box
  • color_field
  • date_field
  • datetime_field
  • datetimelocalfield
  • email_field
  • fields_for
  • file_field
  • form_for
  • hidden_field
  • label
  • number_field
  • password_field
  • radio_button
  • select
  • submit
  • text_area
  • text_field
  • Parameters
  • Request & Response
  • Exposures
  • Rack Integration
  • MIME Types
  • Cookies
  • Sessions
  • Exception Handling
  • Control Flow
  • HTTP Caching
  • Share Code
  • Testing
  • Layouts
  • Custom Error Pages
  • HTML 5 Helpers
  • Routing Helpers
  • Number Helpers
  • Markup Escape Helpers
  • Asset Helpers
  • Custom Helpers
  • Mailers
  • Mailer Tests
  • Asset Pre-processing
  • Asset Compressing
  • CDN Asset Delivery
  • CLI

Explicit
over
Implicit

 
Preference
 

What Next?

Thanks!

A Handy Look at Hanami

By Jason Charnes

Loading comments...

More from Jason Charnes