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

Rails Plugin Authors' Guide

Rails Plugin Authors' Guide

Slides for RubyConf Philippines 2016 talk "Rails Plugin Authors' Guide" http://rubyconf.ph/


Akira Matsuda

April 08, 2016


  1. Rails Plugins Authors' Guide Akira Matsuda

  2. self name: Akira from: Tokyo, Japan GitHub: amatsuda Twitter: @a_matsuda

  3. I Work on Some OSS Projects

  4. I Work on Some OSS Projects Ruby

  5. I Work on Some OSS Projects Ruby Rails

  6. Gems kaminari active_decorator motorhead stateful_enum action_args (asakusarb) Find more on

    http:/ /trending.rubyasia.com/
  7. None
  8. Asakusa.rb Meetup https:/ /twitter.com/lindaliukas/status/717379600175603712

  9. RubyKaigi = ͟ ͟͞͞

  10. RubyKaigi 2015, 2016 Chief Organizer

  11. RubyKaigi 2016 September 8..10 In Kyoto

  12. Coming Soon! CFP Sponsorships Tickets

  13. begin

  14. Rails Plugins

  15. Who Here Have Never Created and Published a Rails Plugin?

  16. What Is a Rails Plugin? A RubyGem Adds some features

    to your Rails apps Written in Ruby (or C)
  17. History of Rails Plugins

  18. Rails 1, Rails 2 Placed in vendor/plugins directory No package

    management Usually served via SVN server on the Internet % ./script/plugin install svn:/ /...
  19. Rails 2.1 Ruby Gem as a Rails plugin config.gem in

    config/ environment.rb "Dependency Hell" problem
  20. Rails 2 Era GitHub hosted gems http:/ /gems.github.com as a

    gem source username-gemname as a Gem config.gem 'mislav-will_paginate', :lib => 'will_paginate'
  21. Rails 3.0 The Rails 3 team (@carlhuda) created Bundler No

    more dependency hell!
  22. Rails 4.0 vendor/plugins' Death

  23. Dealing with Plugins Is So Easy Today Your Gemfile.lock knows

    everything bundle show will take you to any bundled gem An IDE like RubyMine would also help exploring the gems
  24. Plugins Are Your App Every Rails plugin that you bundle

    to your app is a part of your application code You need to read them, understand them, patch them, and sometimes maintain them
  25. Learn How It Works So you can read the code,

    and understand the code
  26. A Typical Rails Plugin's Structure .gemspec lib exe (bin) test,

    spec ext (C extension source code)
  27. How Do Rails Plugins Work? required from the Rails app

  28. require

  29. Ruby's require require 'foo'

  30. require and $LOAD_PATH require 'foo' scans through each $LOAD_PATH directory

    until the first 'foo.rb' (or .so, .o, .dll) were found
  31. RubyGems' require If it fails to require 'foo' And there's

    a Gem named foo installed Adds foo Gem's lib directory to $LOAD_PATH, and then requires it
  32. RubyGems Redefines require Behaviour This is made possible because require

    is nothing but just a Ruby method (Kernel#require). So you can monkey-patch and add a such feature
  33. Bundler's setup & require bundler also extends Kernel#require

  34. Bundler's setup & require Adds the bundled Gems' lib directories

    to $LOAD_PATH requires only from $LOAD_PATH. Not from all the installed gems
  35. When Requiring 'foo' Each bundled Gem's lib directory is in

    $LOAD_PATH And require tries to find 'foo.rb' in each $LOAD_PATH
  36. autoload autoload :Foo, './foo.rb'

  37. Ruby's autoload A technique to save memory usage, shorten the

    app's startup time
  38. Kernel#autoload Takes a Symbol and a filename "Registers filename to

    be loaded (using Kernel::require) the first time that module (which may be a String or a symbol) is accessed in the namespace of mod." https:/ /github.com/ruby/ruby/blob/trunk/ load.c
  39. Rails's autoload Convention When a constant is missing, Rails automatically

    looks up the autoload paths for the constant_name.underscore e.g. User.find(id) but User was still not loaded, Rails automatically finds app/models/ user.rb and loads it So we don't have to manually write require everywhere
  40. The Entry Point of a Gem gemdir/lib/gemname.rb This file will

    be loaded when require 'foo' was executed (usually performed by Bundler) When you read a plugin code, always start reading from this file You can put lib/bar.rb, lib/baz.rb or anything, but that would break other Gems. Do NEVER do that.
  41. The Gem Namespace For the Foo gem, Foo module becomes

    the gem's namespace gemdir/lib/foo/ directory is where you can physically put the code You can create any directories under your gemdir/lib/ directory, but don't create anything else Do never invade other plugins' namespace
  42. _ and - _ just connects multiple words action_args =>

    'action_args' - is for namespacing. Usually when extending another existing gem. kaminari-sinatra => 'kaminari/ sinatra'
  43. Now You Know How to Read You learned basic rules

    of RubyGems Then the next step is to know how to write
  44. Examples

  45. Extending Rails’ Components

  46. Railties, Initializers, and AS.on_load hooks ActiveSupport.on_load :action_controller do (do something)

  47. AS.on_load hooks The block inside AS.on_load will be executed when

    AC::Base was loaded
  48. Extending ActionPack action_args

  49. action_args https:/ /github.com/asakusarb/ action_args Rails version of merb-action-args def show(id)

    User.find id
  50. Adding Parameters to Controller Methods module ActionArgs module AbstractControllerMethods def

    send_action(method_name, *args) return super unless args.empty? return super unless defined?(params) strengthen_params! method_name values = extract_method_arguments_from_params method_name super method_name, *values ennnd
  51. Extending ActionView html5_validators

  52. html5_validators https:/ /github.com/ amatsuda/html5_validators Model validation => HTML5 Form Validation

  53. Adding an Attribute to the text_field Helper module Html5Validators::ActionViewExtension::PresenceValidator def

    render if object.class.ancestors.include?(ActiveModel::Validations && ... @options["required"] ||= @options[:required] || object.class.attribute_required?(@method_name) end super ennd ActionView::Helpers::Tags::TextField.prepend Html5Validators::ActionViewExtension::PresenceValidator
  54. Extending ActiveRecord stateful_enum

  55. stateful_enum https:/ /github.com/amatsuda/ stateful_enum A state machine plugin on top

    of AR::Enum Extends enum method to take a block
  56. Extending AR::Base module StatefulEnum class Railtie < ::Rails::Railtie ActiveSupport.on_load :active_record

    do ::ActiveRecord::Base.extend StatefulEnum::ActiveRecordEnumExtension end end end
  57. Adding a Feature to enum module StatefulEnum module ActiveRecordEnumExtension def

    enum(definitions, &block) enum = super definitions if block definitions.each_key do |column| states = enum[column] StatefulEnum::Machine.new self, column, (states. (Hash) ? states.keys : states), prefix, suffix, &block ennnnnd
  58. Creating a New Layer active_decorator

  59. active_decorator https:/ /github.com/ amatsuda/active_decorator RAILS_ROOT/app/ decorators/ as better view helpers

  60. app/* for a New Layer Nothing special It should just

    work Rails would automatically load the files there
  61. Generator i18n_generators

  62. i18n_generators https:/ /github.com/ amatsuda/i18n_generators Internationalizes your Rails app

  63. i18n_generators % rails g i18n tl #=> generates Tagalog translated

    I18n resource
  64. Creating a Generator Place a .rb file with a Rails::Generator::Base

    class inside lib/generators/ directory.
  65. Creating a Generator lib !"" generators # !"" i18n #

    # %"" i18n_generator.rb
  66. Rake Tasks traceroute

  67. traceroute https:/ /github.com/ amatsuda/traceroute A simple Rake task that reports

    unused routes and controller actions in the app
  68. Creating a Rake Task Place a .rake file inside lib/

    tasks directory.
  69. Creating a Rake Task lib !"" tasks # %"" traceroute.rake

    %"" traceroute.rb
  70. Rails Engine erd

  71. Erd https:/ /github.com/amatsuda/erd Database ER diagram viewer / drawer at

    http:/ /localhost:3000/erd A Rails Engine just looks like a Rails app lib/erd/engine.rb
  72. Erd erd !"" Gemfile !"" app # !"" assets #

    !"" controllers # # %"" erd # # %"" erd_controller.rb # %"" views # %"" erd # %"" erd # %"" index.html.erb !"" config # %"" routes.rb !"" erd.gemspec !"" lib # !"" erd # # !"" engine.rb # # %"" railtie.rb # %"" erd.rb %"" vendor %"" assets
  73. Erd module Erd class Engine < ::Rails::Engine isolate_namespace Erd end

  74. More on Rails Engines Motorhead

  75. Motorhead https:/ /github.com/ amatsuda/motorhead A feature prototyping framework

  76. Prototyping a New Feature Extending an existing controller using Rails

    Engines You can add a new feature prototype without touching the main app.
  77. Prototyping a New Feature class NewFeature::PostsController < ::PostsController def index

    super @something_new = ... ennd
  78. How Motorhead Lets an Engine Intercept the Request module Motorhead

    module Engine extend ActiveSupport::Concern included do isolate_namespace self.parent ActiveSupport.on_load :after_initialize do Rails.application.routes.prepend do mount engine_kls, at: '/' ennnnnd
  79. Some More Rails Plugin Implementation Techniques

  80. AMC vs prepend (Rails 5) alias_method_chain was introduced for Rails

    1 Ruby has Module#prepend as a core feature since Ruby 2.0 alias_method_chain has been deprecated in Rails 5.0 Rails 5 plugins should use Module#prepend instead Rails 5 plugins should drop Ruby 1 support
  81. Designing and Implementing DSL stateful_enum

  82. instance_eval, instance_exec, and block.call Take a look at the code!

  83. Do Never Monkey-patch Core Classes! SimpleCov https:/ /github.com/ colszowka/simplecov/pull/ 449

  84. Do Never Monkey-patch Core Classes Array.send :include, SimpleCov::ArrayMergeHelper Hash.send :include,

  85. Instead,

  86. Instead, Use Refinements!

  87. Refinements as a Better monkey-patching Strategy # lib/action_args/params_handler.rb module ActionArgs

    module ParamsHandler refine AbstractController::Base do def extract_method_arguments_from_params(method_ name) ... ennnnd
  88. Refinements as a Better monkey-patching Strategy using ActionArgs::ParamsHandler module ActionArgs

    module AbstractControllerMethods def send_action(method_name, *args) ... values = extract_method_arguments_from_params method_name super method_name, *values ennnd AbstractController::Base.send :prepend, ActionArgs::AbstractControllerMethods
  89. rbenv Plugin && Gem in One Codebase gem-src

  90. gem-src https:/ /github.com/amatsuda/gem-src A RubyGems hook that git clones every

    repo that you gem install or bundle install Makes you ready to send PRs anytime
  91. gem-src # etc/rbenv.d/exec/~gem-src.bash cwd="$PWD" cd "${BASH_SOURCE%/*}/../../../lib" # Make sure `rubygems_plugin.rb`

    is discovered by RubyGems by adding its directory to Ruby's load path. export RUBYLIB="$PWD:$RUBYLIB" cd "$cwd"
  92. Naming Gems is Hard There's a certain reason that people

    tend to choose weird Gem names Because a Gem name becomes a top-level Module We need to choose a name that would not overlap with model names in the apps Learn from Journey's case
  93. "Namespace Conflict: Journey is a very generic name" https:/ /github.com/rails/

    journey/issues/49 ::Journey => ActionDispatch::Journey
  94. Merging Your Plugin into Rails Core everywhere

  95. everywhere https:/ /github.com/ amatsuda/everywhere where.not syntax for ActiveRecord 3

  96. I Created the Gem First, Proved that It Works, Then

    Sent a PR https:/ /github.com/rails/ rails/pull/8332
  97. Have Fun with
 Rails Plugins! Read the plugins that you

    use Hack on your ideas Solve your problems Share your solutions!
  98. end