Slide 1

Slide 1 text

The Pragmatic Hanami Nov. 29. 2017 kbaba1001

Slide 2

Slide 2 text

BIO ● kbaba1001(Kazuki Baba) ● Ruby 6 years ● Digital nomad ● Rubyist Magazine ○ HanamiはRubyの救世主(メシア)となるか、愚かな星と散る のか

Slide 3

Slide 3 text

Today, talk about... ● Hanami Introduction ● Hanami Architecture ● Hanami Tips

Slide 4

Slide 4 text

Hanami Introduction

Slide 5

Slide 5 text

What is Hanami ? ● Full-stack Ruby web framework ● DDD (Domain Driven Design) ● April 06, 2017, release v1.0.0

Slide 6

Slide 6 text

Hanami vs Rails ● Maintainability ● Pure Object (Zero monkey-patching) ● Multiple application support

Slide 7

Slide 7 text

Hanami depends on gems ● Rack ● Tilt ● DRY rb ● ROM rb ● Sequel

Slide 8

Slide 8 text

Hanami Architecture

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Hanami project directories project_name/ ├── apps/ │ ├── admin/ │ └── web/ ├── config/ ├── db/ ├── lib/ │ ├── project_name/ │ └── project_name.rb ├── spec/ ├── public/

Slide 11

Slide 11 text

“apps” and “lib” directories apps/web/ ├── application.rb ├── assets/ ├── config/ ├── controllers/ ├── templates/ └── views/ lib/ ├── project_name/ │ ├── entities/ │ ├── repositories/ │ └── interactors/ └── project_name.rb Application Layer Domain Layer

Slide 12

Slide 12 text

Application Layer

Slide 13

Slide 13 text

Application Layer ● Routes ● Controller ● View/Template

Slide 14

Slide 14 text

Routes get '/proc', to: ->(env) { [200, {}, ['Hello from Hanami!']] } get '/action', to: "home#index" get '/middleware', to: Middleware get '/rack-app', to: RackApp.new get '/rails', to: ActionControllerSubclass.action(:new)

Slide 15

Slide 15 text

RESTful Resources resources :books, only: [:new, :create, :show] resource :account

Slide 16

Slide 16 text

Routes file path ● Hanami ○ apps/web/config/routes.rb ○ apps/admin/config/routes.rb ○ … and so on ● Rails ○ config/routes.rb

Slide 17

Slide 17 text

Controller module Web::Controllers::Diaries class New include Web::Action def call(params) #=> [status, header, body] end end end

Slide 18

Slide 18 text

View ● View ○ Ruby Class ● Template ○ HTML Template (erb/haml/slim...)

Slide 19

Slide 19 text

View Class module Web::Views::Dashboard class Index include Web::View def title 'Dashboard' end end end

Slide 20

Slide 20 text

Template

<%= title %>

%h1= title Erb Haml

Slide 21

Slide 21 text

Domain Layer

Slide 22

Slide 22 text

Domain Layer ● Model ○ Entity ○ Repository ● Service (Interactor)

Slide 23

Slide 23 text

Generate model Run hanami g model foo command, generate: ● migration ● entity ● repository

Slide 24

Slide 24 text

Migration # db/migrations/20170908025424_create_diaries.rb Hanami::Model.migration do change do create_table :diaries do primary_key :id column :body, String, null: false column :title, String column :created_at, DateTime, null: false column :updated_at, DateTime, null: false end end end

Slide 25

Slide 25 text

Entity class Diary < Hanami::Entity end diary = Diary.new(title: 'learn ruby') diary.title #=> "learn ruby" diary.title = 'learn english' #=> NoMethodError: undefined method `title='

Slide 26

Slide 26 text

Repository class DiaryRepository < Hanami::Repository associations do has_many :comments end def find_with_comment(id) aggregate(:comments).where(diaries_id: id).map_to(Diary).one end end

Slide 27

Slide 27 text

Service(Interactor) module DiaryInteractor class Create include Hanami::Interactor expose :params, :diary def initialize(params) @params = params end def call @diary = DiaryRepository.new.create(params) end end end

Slide 28

Slide 28 text

Hanami Tips

Slide 29

Slide 29 text

Validation

Slide 30

Slide 30 text

Validation (in action) module Web::Controllers::Users class Create include Web::Action params do required(:user).schema do required(:email) { filled? } end end # … end end

Slide 31

Slide 31 text

Issue params do # do not work predicate :email?, message: 'invalid email format' do |value| # ... end required(:user).schema do required(:email) { filled? & email? } end end

Slide 32

Slide 32 text

Issue params Class.new(Hanami::Action::Params) do predicate :email?, message: 'invalid email format' do |value| # ... end validations do required(:user).schema do required(:email) { filled? & email? } end end end

Slide 33

Slide 33 text

Issue params Class.new(Hanami::Action::Params) do predicate :email?, message: 'invalid email format' do |value| # ... end validations do required(:user).schema do required(:email) { filled? & email? } end end end

Slide 34

Slide 34 text

Issue ● depends on the implementation of Hanami::Action#params ● Class.new( ) { } is not readable

Slide 35

Slide 35 text

Solution ● Independent validation class ● Validate with service instead of action

Slide 36

Slide 36 text

Independent validation class class DiaryInteractor::Create::Validation include Hanami::Validations predicate :email?, message: 'invalid email format' do |value| # ... end validations do required(:email) { filled? & email? } end end

Slide 37

Slide 37 text

Service Layer class DiaryInteractor::Create include Hanami::Interactor expose :params, :diary def initialize(params) @params = params end def call @diary = DiaryRepository.new.create(params) end def valid? DiaryInteractor::Create::Validation.new(@params).validate.success? end

Slide 38

Slide 38 text

Timezone

Slide 39

Slide 39 text

Timezone # config/initializers/sequel.rb Sequel.application_timezone = :tokyo Sequel.database_timezone = :utc Sequel.typecast_timezone = :utc

Slide 40

Slide 40 text

i18n

Slide 41

Slide 41 text

initializer # config/initializers/i18n.rb require 'i18n' require 'i18n/debug' if ENV['I18N_DEBUG'] == 'true' I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) I18n.load_path = Dir[ Hanami.root.join('config/locales/*.yml').to_s, Hanami.root.join('config/locales/**/*.yml').to_s ] I18n.backend.load_translations I18n.enforce_available_locales = false I18n.config.default_locale = 'ja'

Slide 42

Slide 42 text

view helper module LocaleHelper def t(key, options = {}) ::I18n.t(key, default_options(key).merge(options)) end def default_options(key) if key.start_with?('.') app, _, controller, action = self.class.name.split('::').map {|class_name| Hanami::Utils::String.new(class_name).underscore } {scope: "#{app}.#{controller}.#{action}"} else {} end

Slide 43

Slide 43 text

Validation class module AbstractValidation def self.included(klass) klass.class_eval do include Hanami::Validations messages :i18n end end end class DiaryInteractor::Create::Validation include AbstractValidation end

Slide 44

Slide 44 text

Webpack

Slide 45

Slide 45 text

webpack ● Output builded files under public directory ● Use webpack-manifest-plugin ○ output manifest.json ● Load output files to view template

Slide 46

Slide 46 text

manifest.json example { "web.css": "web-a95de7f402e5c515bf63.css", "web.js": "web-a95de7f402e5c515bf63.js" }

Slide 47

Slide 47 text

view helper module ViewHelper def webpack_asset_path(filepath) manifest = JSON.parse(manifest_filepath.read) bundled_filename = manifest[filename.to_s] raw(Hanami.public_directory.join(bundled_filename)) end end

Slide 48

Slide 48 text

Alternative ActiveSupport

Slide 49

Slide 49 text

Alternative ActiveSuport ● Hanami::Util ○ https://github.com/hanami/utils ● Time Math 2 ○ https://github.com/zverok/time_math2

Slide 50

Slide 50 text

Hanami::Util https://github.com/hanami/utils

Slide 51

Slide 51 text

Hanami::Util Example require 'hanami/utils/blank' Hanami::Utils::Blank.blank?('') # => true Hanami::Utils::Blank.blank?(nil) # => true Hanami::Utils::Blank.blank?('a') # => false

Slide 52

Slide 52 text

Time Math 2 Time Math 2: TimeMath(Time.now).floor(:day).decrease(:week, 1).call ActiveSupport: 1.week.ago.beginning_of_day

Slide 53

Slide 53 text

conclusion

Slide 54

Slide 54 text

Today, talk about... ● Hanami Introduction ● Hanami Architecture ● Hanami Tips

Slide 55

Slide 55 text

important!! Please give me a Hanami’s job. http://www.gupipo.co.jp/