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
Tweet

More Decks by Akira Matsuda

Other Decks in Programming

Transcript

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

    to your Rails apps Written in Ruby (or C)
  2. Rails 1, Rails 2 Placed in vendor/plugins directory No package

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

    config/ environment.rb "Dependency Hell" problem
  4. 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'
  5. 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
  6. 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
  7. require and $LOAD_PATH require 'foo' scans through each $LOAD_PATH directory

    until the first 'foo.rb' (or .so, .o, .dll) were found
  8. 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
  9. 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
  10. Bundler's setup & require Adds the bundled Gems' lib directories

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

    $LOAD_PATH And require tries to find 'foo.rb' in each $LOAD_PATH
  12. 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
  13. 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
  14. 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.
  15. 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
  16. _ and - _ just connects multiple words action_args =>

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

    of RubyGems Then the next step is to know how to write
  18. 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
  19. 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
  20. Extending AR::Base module StatefulEnum class Railtie < ::Rails::Railtie ActiveSupport.on_load :active_record

    do ::ActiveRecord::Base.extend StatefulEnum::ActiveRecordEnumExtension end end end
  21. 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
  22. app/* for a New Layer Nothing special It should just

    work Rails would automatically load the files there
  23. 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
  24. 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
  25. Prototyping a New Feature Extending an existing controller using Rails

    Engines You can add a new feature prototype without touching the main app.
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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"
  32. 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
  33. "Namespace Conflict: Journey is a very generic name" https:/ /github.com/rails/

    journey/issues/49 ::Journey => ActionDispatch::Journey
  34. I Created the Gem First, Proved that It Works, Then

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

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