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

Designing and Implementing Modern Rails Plugins

Designing and Implementing Modern Rails Plugins

Slides for RailsClub 2016 talk "Designing and Implementing Modern Rails Plugins" http://railsclub.ru/


Akira Matsuda

October 22, 2016


  1. Designing and Implementing Modern Rails Plugins Akira Matsuda

  2. self Akira Matsuda GitHub: amatsuda Twitter: @a_matsuda Ruby & Rails

    developer "
  3. Matz! Akira Matsuda (দా) GitHub: amatsuda Twitter: @a_matsuda Ruby &

    Rails developer "
  4. "Modern" Rails Plugins

  5. Agenda Dealing with Frameworks Through Adapters Automagic Standing on Shoulders

    of Rails Using Modern Ruby Features Make Rails Engines Great Again From Omakase to Okonomi
  6. Dealing with Frameworks Through Adapters

  7. Kaminari ⚡ /amatsuda/kaminari Adds some pagination methods to models Provides

    some view helpers Supports multiple ORMs and web frameworks
  8. Layers M and V Implemented as a Rails Engine Actually

    one of the first generation Rails Engine plugins Devise 1.1.0: July 25, 2010 Kaminari 0.1.0: February 5, 2011
  9. Supported Frameworks 0.1: Active Record & Action View (default Rails

    stack) 0.9.13: Mongoid 0.12.2: MongoMapper 0.13.0: DataMapper 0.13.0: Sinatra 0.14.0: Grape
  10. How Kaminari Loads the Frameworks No dependency in the gemspec

    Just `require` from the ruby code if defined?(the framework) then load the adapter
  11. lib/kaminari/ hooks.rb (0.16.x) module Kaminari class Hooks def self.init ...

    begin; require 'mongoid'; rescue LoadError; end if defined? ::Mongoid require 'kaminari/models/mongoid_extension' ::Mongoid::Document.send :include, Kaminari::MongoidExtension::Document ennnnd
  12. Such a Nasty Code! Just ugly ::Mongoid can be defined

    even if not using Mongoid gem Users have to install some unrelated code that they don't use Takes so much time to run CI
  13. The CI Problem kaminari 0-17-stable branch runs CI against Ruby

    1.9.3, 2.0.0, 2.1.10, 2.2.5, 2.3.1, jruby-1.7.25, jruby-, rbx-2 active_record_30, active_record_31, active_record_32, active_record_40, active_record_41, active_record_42, active_record_edge, data_mapper_12, mongo_mapper, mongoid_24, mongoid_30, mongoid_31, mongoid_40, mongoid_50, sinatra_13, sinatra_14 Wasting Travis's resource
  14. The Build Matrix Runs 110 jobs per build!

  15. The Build Matrix

  16. Separating Core and Adapters So I decided to split up

    the huge monolithic gem into the core gem and adapter gems
  17. Kaminari 1.0 Structure kaminari-core ORM adapters View adapters

  18. Bundling Kaminari with Active Record + Sinatra gem 'kaminari-activerecord' gem

  19. Bundling Kaminari with Mongoid + Action View (Rails) gem 'kaminari-mongoid'

    gem 'kaminari-actionview'
  20. Bundling Kaminari with Active Record + Action View (the Default

    Rails Stack) gem 'kaminari-activerecord' gem 'kaminari-actionview'
  21. Active Record + Action View = the Default Rails Stack

    99% of Kaminari users should be using Kaminari with these gems This should be able to be done easier
  22. Using Kaminari with Active Record + Action View (Rails) gem

    'kaminari-activerecord' gem 'kaminari-actionview'
  23. Using Kaminari with Active Record + Action View (Rails) #

    gem 'kaminari-activerecord' # gem 'kaminari-actionview' gem 'kaminari'
  24. kaminari Gem A meta package that depends on kaminari-activerecord and

    kaminari-actionview Just like rails gem and rspec gem
  25. Compatibilty Those who uses kaminari with the default Rails stack

    doesn't have to change what's written in Gemfile
  26. Q. What If I Need a Pagination Library for Hanami?

    Just create kaminari-hanami gem! And let me know! I'm happy to put it under /kaminari organization
 (if you'd like to) This should be much easier now, I guess
  27. 1.0 Plan Pretty close! Some PRs to merge, some issues

    to solve We'll move the GH repo under kaminari organization Then release 1.0
  28. The GitHub Organization /kaminari

  29. Maintaining a Gem Is Hard I create gems to solve

    my own problems Dealing with PRs/issues is to deal with other people's problems (Without being paid) Which is not always fun Maybe I'm not a very good gem maintainer
  30. A Software Needs "Mainainters" Just like nobu for ruby, and

    rafael for rails Luckily, Kaminari has @yuki24
  31. @yuki24 the CIO "Chief Issue triage Officer" Creator of ruby

    2.3's did_you_mean feature
  32. did_you_mean % ruby -rkaminari -e 'Kaminair' -e:1:in `<main>': uninitialized constant

    Kaminair (NameError) Did you mean? Kaminari
  33. Kaminari kaminari 1.0 gem is coming soon! We're maintaining kaminari

    and kaminari-* gems as a team Join us! PRs are welcome!
  34. Automagic

  35. Just bundle, Then It Should Work Don't let the users

    configure for the plugin Don't let the users include some special Modules from the plugin Don't let the users call some weird method to let the plugin work These could be done via callbacks that Rails provides
  36. Let Me Show You an Example of an Automagic Gem

  37. /amatsuda/ active_decorator Provides a decorator layer to Rails apps app/decorators/*_decorator.rb

    A decorator is essentially a view helper
 (alternative to app/helpers/*_helper.rb) A Decorator is a pure Ruby Module Mixed in to model instances via `Module#extend` Automagically only in Action View's context
  38. Alternative Draper Actually when I was looking for a decorator

    library, I found this
  39. Draper Example # app/controllers/articles_controller.rb def show @article = Article.find(params[:id]).decorate end

    # app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator delegate_all def publication_status published? ? "Published at #{published_date}" : "Unpublished" end def published_date object.published_at.strftime("%A, %B %e") end def emphatic h.content_tag(:strong, "Awesome") end end
  40. Firstly I Considered About Using It, But I Didn't Like

    Its Design API Implementation
  41. I Mean, Everything

  42. So I Decided to Build My Own Decorator Library

  43. Why Didn't I Send PRs to the Existing One? People

    ask me "why did you create your own paginater/decorator?" I DO sending PRs! Of course I usually consider doing this first
  44. When to PR, When to Fork, and When to Build

    Your Own? You just found a bug, and you could fix it => send a PR You basically liked it, but you wanted to add big changes => fork You didn't like its design/philosophy => build your own
  45. When to Just Raise an Issue? Basically, don't. Write a

    patch instead of filling the textarea with words If you couldn't fix the bug you've found, just filing a failing test case is totally fine
  46. Anyway, I didn't like Draper's design/ philosophy So I built

    my own
  47. I Said, A Decorator Is a View Helper Draper decorates

    in the Controller layer Which is IMO wrong Decorators should automatically work in view files Just like *_helpers.rb Who wants to write `include ApplicationHelper` in every view file? Explicitly calling `.decorate` method is like doing this
  48. Requirements Decorators should not be enabled in models/ controllers context

    Decorators should be automatically enabled in views context Without adding anything to the users' application code How can we achieve this?
  49. Techniques Behind ActiveDecorator's Automagicness Conventions Callbacks Monkey-patches

  50. Convention over Configuration A decorator module for Article class should

    be named ArticleDecorator, and be placed in app/decorators/article_decorator.rb The Rails way that you're all familiar with Everyone likes this over writing something like "config/decorators.xml", right?
  51. Callbacks module ActiveDecorator class Railtie < ::Rails::Railtie initializer 'active_decorator' do

    ActiveSupport.on_load :action_controller do require 'active_decorator/monkey/ abstract_controller/rendering' ::ActionController::Base.send :prepend, ActiveDecorator::Monkey::AbstractController::Rendering ennnnd
  52. AS.on_load Callback Executes the block right after ActionController::Base (in this

    example) is loaded So the users don't have to write include/prepend in their applications Very common technique
  53. Monkey-patches

  54. What We're Going to Do We want to decorate objects

    that are passed from controllers to views Also, we want to decorate collections Decorate each model instance in AR::Relation objects that are passed from controllers to views But we don't want ActiveDecorator to kick AR queries because of doing this
  55. Decorating Objects in Between Conrollers and Views All controller ivars

    should be accessed via `view_assigns` as a Hash So let's just override this method to decorate each value
  56. Monkey-patching view_assigns module ActiveDecorator module Monkey module AbstractController module Rendering

    def view_assigns hash = super hash.each_value do |v| ActiveDecorator::Decorator.instance.decorate v end hash ennnnnd
  57. What About Collections?

  58. Decorating Each Model in AR::Relation Instances Between Conrollers and Views

    - @articles.each do |article| %p= article.title %p= article.published_date
  59. Could We Just Extend the view_assigns Monkey-patch?

  60. Like This? module ActiveDecorator::Monkey::AbstractController::Rendering def view_assigns hash = super hash.each_value

    do |v| if v.is_a? ActiveRecord::Relation v.each do |record| ActiveDecorator::Decorator.instance.decorate record end else ActiveDecorator::Decorator.instance.decorate v end end hash ennd
  61. Could We Just Extend the view_assigns Monkey-patch?

  62. This Breaks POLS "Principal of Less Side-effects" Rails plugins should

    avoid causing unwanted side effects If a decorator fires AR queries, that's a serious side effect
  63. The Solution Double monkey-patching When decorating an AR::Relation, extend its

    `records` method to dynamically decorate each record
  64. Like This! module ActiveDecorator class Decorator def decorate(obj) ... if

    defined?(ActiveRecord) && obj.is_a?(ActiveRecord::Relation) && !obj.is_a? (ActiveDecorator::RelationDecorator) if obj.respond_to?(:records) obj.extend ActiveDecorator::RelationDecorator end else d = decorator_for obj.class return obj unless d obj.extend d unless obj.is_a? d end end module RelationDecorator def records super.tap do |arr| ActiveDecorator::Decorator.instance.decorate arr end end ennd
  65. These Techniques Makes This Possible # app/controllers/articles_controller.rb def show @article

    = Article.find(params[:id]) end # app/decorators/article_decorator.rb class ArticleDecorator def publication_status published? ? "Published at #{published_date}" : "Unpublished" end def published_date published_at.strftime("%A, %B %e") end def emphatic content_tag(:strong, "Awesome") end end
  66. Instead of This # app/controllers/articles_controller.rb def show @article = Article.find(params[:id]).decorate

    end # app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator delegate_all def publication_status published? ? "Published at #{published_date}" : "Unpublished" end def published_date object.published_at.strftime("%A, %B %e") end def emphatic h.content_tag(:strong, "Awesome") end end
  67. Without Poluting the App with These Unneeded Extra Codes #

    app/controllers/articles_controller.rb def show @article = Article.find(params[:id]).decorate end # app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator delegate_all def publication_status published? ? "Published at #{published_date}" : "Unpublished" end def published_date object.published_at.strftime("%A, %B %e") end def emphatic h.content_tag(:strong, "Awesome") end end
  68. It's Automagic

  69. Using Modern Rails Features

  70. AR::Enum in Rails 4.1 Added to AR by DHH Persists

    values into DB in Integer Provides human-readable methods and scopes
  71. I Basically Liked This Feature We very often hit this

    use-case in real-world apps It works But something is missing
  72. When Defining Enums in the Apps We very often want

    to define state machines
  73. State Machine in Rails We were using some kind of

    state machine plugins
  74. State Machine Plugins in Rails AASM state_machine Who uses them?

  75. Did You Know That Rails Once Tried to Merge AASM

    as ActiveModel::StateMachine?
  76. We Tried, /rails/rails/commit/b9528ad

  77. But We Gave Up, /rails/rails/commit/db49c70

  78. Then Never Came Back Again... "We're going do it eventually,

    get it done before 3.0 is final."
  79. AASM, state_machine Very "Heavy" design Providing DSL, defining ruby methods,

    holding values in Objects, persisting to DBs, etc.
  80. % tree lib/aasm lib/aasm !"" aasm.rb !"" base.rb !"" configuration.rb

    !"" core # !"" event.rb # !"" state.rb # %"" transition.rb !"" dsl_helper.rb !"" errors.rb !"" instance_base.rb !"" localizer.rb !"" persistence # !"" active_record_persistence.rb # !"" base.rb # !"" core_data_query_persistence.rb # !"" dynamoid_persistence.rb # !"" mongo_mapper_persistence.rb # !"" mongoid_persistence.rb # !"" plain_persistence.rb # !"" redis_persistence.rb # %"" sequel_persistence.rb !"" persistence.rb !"" rspec # !"" allow_event.rb # !"" allow_transition_to.rb # !"" have_state.rb # %"" transition_from.rb !"" rspec.rb !"" state_machine.rb !"" state_machine_store.rb %"" version.rb
  81. % tree lib/ state_machine lib/state_machine !"" assertions.rb !"" branch.rb !""

    callback.rb !"" core.rb !"" core_ext # %"" class # %"" state_machine.rb !"" core_ext.rb !"" error.rb !"" eval_helpers.rb !"" event.rb !"" event_collection.rb !"" extensions.rb !"" graph.rb !"" helper_module.rb !"" initializers # !"" merb.rb # %"" rails.rb !"" initializers.rb !"" integrations # !"" active_model # # !"" locale.rb # # !"" observer.rb # # !"" observer_update.rb # # %"" versions.rb # !"" active_model.rb # !"" active_record # # !"" locale.rb # # %"" versions.rb # !"" active_record.rb # !"" base.rb # !"" data_mapper # # !"" observer.rb # # %"" versions.rb # !"" data_mapper.rb # !"" mongo_mapper # # !"" locale.rb # # %"" versions.rb # !"" mongo_mapper.rb # !"" mongoid # # !"" locale.rb # # %"" versions.rb # !"" mongoid.rb # !"" sequel # # %"" versions.rb # %"" sequel.rb !"" integrations.rb !"" machine.rb !"" machine_collection.rb !"" macro_methods.rb !"" matcher.rb !"" matcher_helpers.rb !"" node_collection.rb !"" path.rb !"" path_collection.rb !"" state.rb !"" state_collection.rb !"" state_context.rb !"" transition.rb !"" transition_collection.rb !"" version.rb !"" yard # !"" handlers # # !"" base.rb # # !"" event.rb # # !"" machine.rb # # !"" state.rb # # %"" transition.rb # !"" handlers.rb # !"" templates # # %"" default # # %"" class # # %"" html # # !"" setup.rb # # %"" state_machines.erb # %"" templates.rb %"" yard.rb
  82. One Day, I Had to Implement a State Machine on

    My Rails 4 App Where we already had an Enum And I came up with an idea "What if I build a state machine feature on top of AR::Enum?"
  83. What AR::Enum Already Provides A simple syntax to define key

    & value pairs on AR models Persists the values to the backend DB Defines some human-readable query methods and methods to update values
  84. What We Need to Add on It State transitioning methods

    State checks Callbacks
  85. Half of the State Machine Features Are Already Implemented in

    AR::Enum I thought I could build a state machine library that I need in less code
  86. And So I Made a Gem stateful_enum

  87. /amatsuda/ stateful_enum Extends AR's `enum` method to take a block

    Uses what I like about AR::Enum, fixes what I don't like, and adds what are missing
  88. Likes And Don't Likes About AR::Enum Method names DB design

  89. Method Names class Conversation < ActiveRecord::Base enum status: [ :active,

    :archived ] end # conversation.update! status: 0 conversation.active! conversation.active? # => true # conversation.update! status: 1 conversation.archived! conversation.archived? # => true
  90. No! Method Names Should Be Verbs! A method to change

    the status to :active should be `activate`
  91. DB Design I didn't like the DB design of existing

    state machine gems They tend to save the state names in VARCHAR into DB I prefer persisting INTEGER values just like AR::Enum does
  92. stateful_enum On top of AR::Enum Supports AR only Extremely short

  93. Size of State Machine Plugins % cat aasm/lib/**/*.rb | wc

    -l 2589 % cat state_machine/lib/**/*.rb | wc -l 10671 % cat stateful_enum/lib/**/*.rb | wc -l 226
  94. Amazingly Short Code Because I didn't need to build everything

    from scratch statuful_enum stands on shoulders of AR::Enum And we don't support multiple ORMs If we need to, we probably will follow the same strategy with kaminari
  95. Actually the Main File Is Just One File lib/stateful_enum/machine.rb Only

    86 LOC without comments and blank lines
  96. Stand on Shoulders of Rails Use Rails' built-in code Rather

    than creating everything from scratch
  97. Write Less Code This is, of course, a Rails' principal

    It's easier to read It's easier to patch So it's easier to maintain
  98. Using Modern Ruby Features

  99. Supported Versions of Ruby & Rails Basically 2-3 generations Because

    both projects do not have enough resources to maintain more than that
  100. Ruby Developing: 2.4 Security & bug fixes: 2.3, 2.2 Security

    fixes: 2.1 2.0 and 1.x: No support. No security updates. Please don't use!
  101. ⚠ macOS Sierra Still bundles 2.0!

  102. Rails Developing: 5.1 Security & bug fixes: 5.0, 4.2 Security:

    4.1 4.0 and 3.x: No support. No security update. Please don't use!
  103. Rails Plugins Supports... Kaminari stable: Ruby 1.9.3+, Rails 3.0+ Haml

    stable: Ruby 1.8.7+, Rails 3.0+
  104. Gems Do Not Have To Provide Such a Wide Support!

    At least, a new major release doesn't have to support unsupported versions of Ruby / Rails Rails aggressively bumps supporting Ruby versions To keep the codebase maintainable To move the community forward I really this attitude Let's do this for your Rails plugins as well!
  105. What Would We Get If We Stop Supporting Old Rubies?

    We can use new features of Ruby!
  106. New Features of Recent Versions of Ruby 2.0: Module#prepend, keyword

    arguments, Refinements 2.1: Module#include / prepend as public methods, Binding#local_variable_get 2.2: Kernel#itself, Method#super_method 2.3: &., Hash#dig
  107. Module#prepend Let me now focus on a relatively recent Ruby

    feature, `Module#prepend`
  108. alias_method_chain vs Module#prepend Rails no longer has AMC Modern plugins

    have to use `Module#prepend` If you find a gem that still doesn't use `Module#prepend`, that should be an outdated gem
  109. If you're a Plugin Maintainer, Always Think of a Chance

    to Use Modern Ruby Features
  110. An Actual Example of Using a Modern Ruby Feature

  111. Refinements Maybe useful, but rarely used feature of Ruby 2

  112. action_args /asakusarb/action_args A plugin to change your action methods look

    more Rubyish
  113. Normal Rails Action class UsersController < ApplicationController def show @user

    = User.find params[:id] end end
  114. With action_args class UsersController < ApplicationController def show(id) @user =

    User.find id end end
  115. Refinements action_args implements this feature by using Refinements internally

  116. How action_args Uses Refinements Defines a Module that refines Rails'

    core Class Uses the Module in the plugin internal Users would never able to access these plugin internal methods
  117. Defining a Module That Refines Rails' Core Class # lib/action_args/params_handler.rb

    module ActionArgs module ParamsHandler refine AbstractController::Base do def extract_method_arguments_from_params(method_name) ... ennnnd
  118. Using the Module in the Plugin Internal 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
  119. These Methods Are Never Exposed to the Users A very

    clean way to
 monkey-patch Ruby/Rails core classes in your plugin
  120. You Can Define
 Absolutely "Plugin private"
 Methods This Way This

    is a very useful technique Check out /asakusarb/ action_args for more details
  121. By Using Recent Ruby Features You may be able to

    make your plugin code cleaner Use new Ruby features! Instead of supporting old Rubies
  122. Make Rails Engines Great Again

  123. Rails Engines Rails Engine is_a Rails plugin Rails Application is_a

    Rails Engine A Rails Engine can contain whole M, V, and C components A Rails Engine can contain routes
  124. Rails Engines Patterns The app and plugins The main app

    and sub apps Shared components and apps
  125. Typical Rails Engine Plugins Mount another Rails app (e.g. admin

    interface) onto users' applications (Not very much technically interesting)
  126. Actually I created one Erd

  127. /amatsuda/erd You can mount an ER diagram app You can

    manipulate the DB schema by editing the models and columns from the browser
  128. Rails Engines Seems Somewhat Useful But still too simple Maybe

    we need some more features to make it truly useful I don't know what exactly Rails Engines is missing. We need to investigate
  129. So, I Made a Product to Investigate a New Usage

    of Rails Engines /amatsuda/motorhead
  130. Motorhead 0 An application feature as a Rails Engine Rails

    Engines for feature prototyping
  131. An Application Feature as a Rails Engine A Rails Engine

    can contain models, views, controllers, and routes Maybe we can implement an independent feature as an Engine
  132. Directory Structures Compared

  133. Normal Rails App normal_rails_app %"" app !"" controllers # !""

    feature1_controller.rb # %"" feature2_controller.rb %"" views !"" feature1 %"" feature2
  134. Motorhead motorhead %"" app %"" engines !"" feature1 # %""

    app # !"" controllers # # %"" feature1_controller.rb # %"" views # %"" feature1 %"" feature2 %"" app !"" controllers # %"" feature2_controller.rb %"" views %"" feature2
  135. Advantage of This Directory Structure It's easier to add /

    remove features Without causing code conflicts within you team
  136. Rails Engines for Feature Prototyping You can overwrite existing controller

    actions You can call the original action from the Engine via super Automatically fallbacks to super on error So we can deploy unstable code to production
  137. Not Only in This Direction But I believe Rails Engines

    can be a better mechanism I'd like to expand the capability of Rails Engines
  138. Timeline First release date of Motorhead: 2015-10-23 Lemmy passed away:

  139. RIP Lemmy

  140. From Omakase To Okonomi Rails is shipped with DHH's assorted

    strong default stack which is called "Omakase" Rails allows users to choose alternative libs, e.g. Sequel instead of AR, Haml instead of ERB I want to expand this a little bit more
  141. We Don't Always Want to Eat Everything Served in the

    Omakase Course
  142. Rather Than Including Everything in Core, Or Making Everything Customizable,

    Provide tiny alternative parts!
  143. Rails Became Mature Enough Supports everything in one codebase Became

    too huge and slow I sometimes miss Rails 1.x simplicity We don't need all Rails features for each of our apps As we're deploying the whole framework, we're loading so many unused things in production Just like Kaminari's case that I showed in this presentation
  144. I Guess We Can Somehow Solve This Problem by Rails

  145. Rather Than Huge Gems That Provide New Features Alternative parts

    that are compatible with the default Rails parts Just like DOS/V PC parts You can choose Intel or AMD, ATI or nvidia, just combine them then that'll make a PC Mainly for speed, memory usage
  146. Establish public APIs Investigate what can be made simpler Encourage

    gem authors to publish alternatives
  147. There Should Be Some Hidden Needs, For Example I miss

    the speed of AR1 query I want a simpler I18n that runs faster. I don't use String interpolations. Our language doesn't have pluralization form. So I18n can be simpler I want English only Rails. No I18n is needed for my app I want AR model instance not to create tons of Objects per each attribute. Just a Hash like AR3 was enough My app doesn't have to support Windows. I want Rails that deals JSON only. My Rails app has no HTML view I don't care about Thread-safeness. I don't use threaded servers
  148. You Could Solve Them With Plugins! Bringing AR1 speed back

    to AR queries A simpler and faster I18n alternative A routing engine Faster view template resolver Faster template renderer that cannot handle any other encodings than UTF8 Faster `url_for` Faster HashWithIndifferentAccess
  149. Sorry Sean, I didn't mean to troll at you

  150. It's Totally Fine to Implement a New Attributes API for

    AR Actually I this improvement! But let's provide a way to define another Attributes API that works as fast as previous version
  151. Rails Core Doesn't Have to Create and Maintain Everything The

    community will work on such alternative parts, if needed
  152. Back to Rails 1, or Merb Let the users be

    able to choose Rails components Cut off what you don't use To enhance performance
  153. "Let Us Choose" There's a word for that in Japanese

  154. That Style Is Called "Okonomi" == A la carte True

    gourmands prefer this style
  155. Maybe We Could Apply This Idea on Ruby Language As

    Well? I don't know. I never seriously thought about that, but maybe
  156. But Anyway,

  157. We Can Make Rails Faster! And simpler!

  158. Summary

  159. "Modern Rails Plugins" Kaminari 1.0 is coming soon! Follow the

    Rails way, Ruby way Keep yourself and your code up to date Keep your code maintainable, sustainable
  160. Happy Hacking!