Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Pragmatic Hanami

kbaba1001
November 30, 2017

The Pragmatic Hanami

kbaba1001

November 30, 2017
Tweet

More Decks by kbaba1001

Other Decks in Technology

Transcript

  1. The Pragmatic Hanami
    Nov. 29. 2017
    kbaba1001

    View Slide

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

    View Slide

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

    View Slide

  4. Hanami Introduction

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. Hanami Architecture

    View Slide

  9. View Slide

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

    View Slide

  11. “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

    View Slide

  12. Application
    Layer

    View Slide

  13. Application Layer
    ● Routes
    ● Controller
    ● View/Template

    View Slide

  14. 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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  20. Template
    <%= title %>
    %h1= title
    Erb
    Haml

    View Slide

  21. Domain
    Layer

    View Slide

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

    View Slide

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

    View Slide

  24. 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

    View Slide

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

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. Hanami Tips

    View Slide

  29. Validation

    View Slide

  30. 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

    View Slide

  31. 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

    View Slide

  32. 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

    View Slide

  33. 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

    View Slide

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

    View Slide

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

    View Slide

  36. 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

    View Slide

  37. 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

    View Slide

  38. Timezone

    View Slide

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

    View Slide

  40. i18n

    View Slide

  41. 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'

    View Slide

  42. 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

    View Slide

  43. 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

    View Slide

  44. Webpack

    View Slide

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

    View Slide

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

    View Slide

  47. 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

    View Slide

  48. Alternative
    ActiveSupport

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  53. conclusion

    View Slide

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

    View Slide

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

    View Slide