-
This pioneering book deep-dives into the Rails plugin APIs and shows you, the intermediate Rails developer, how to use them to write better web applications and make your day-to-day work with Rails more productive.
-
You’ll learn how the Rails rendering stack works and customize it to read templates from the database while you discover how to mimic Active Record behavior, such as validations, in any other object. You’ll find out how Rails integrates with Rack, the different ways to stream data from your web application, and how to mix Rails engines and Sinatra applications into your Rails apps, so you can choose the most appropriate tool for the job. In addition, you’ll improve your productivity by customizing generators and responders.
Rails uses the model-view-controller (MVC) architecture pattern to organize our code.
The common interface to render a given model or template is the render method.
Besides knowing how to render a :template
or a :file
, Rails can render raw :text
and a few formats, such as :xml
, :json
, and :js
.
Explain about the structure of rails plugin
.
Render is nothing more than a hook exposed bu the render
method to customize its behavior.
Playing with Prawn: pdf library for Ruby.
ActionController::Renderers.add :pdf do |filename, options|
pdf = Prawn::Document.new
pdf.text render_to_string(options)
send_data(pdf.render, filename: "#{filename}.pdf", disposition: "attachment")
end
AbstractController was designed in a way that developers can cherry-pick the funcionality they want.
Abstract Controller’s rendering stack is responsible for normalizing the arguments and options you provide and converting them to a hash of options that ActionView::Renderer#render accepts, which will take care of finally rendering the template.
An instance of ActionView::Renderer called view_renderer is created and the render method is called on it with two arguments the view_context and the hash of normalized options.
The view context is an instance of ActionView::Base it is the context in which our templates are evaluated.
Take a look Firgure 3. Visualization of the rendering stack
ActiveModel was originally created to hold the behavior shared between ActiveRecord and ActiveResource. It`s also responsible for defining the API (Application Programming Interface) required by controllers and views.
-
Active Resource (ARes): Connects business objects and Representational State Transfer (REST) web services.
-
ActiveModel::AttributeMethods: is a module that tracks all defined attributes, allowing us to add a commom behavior to all of them dinamically. Provide a way to add suffix and prefix to your methods using
attribute_method_prefix
,attribute_method_suffix
,define_attribute_methods
andprefix_attribute
orattribute_suffix
methods definitions. -
ActiveModel::Conversion: Handles default conversions:
#to_model
: Rails controllers and view helpers manipulate the model using to_model instead of the model directly;#to_key
: return an array of keys that uniquely identifies the model;#to_param
: used in rounting;#to_partial_path
: is invoked every time we pass a record or a collection of records to render in our views;
“This not only makes our code cleaner, but also improves our application performance”
-
ActiveModel::Naming: Creates a
model_name
to your model, acts like a String and provide a few methods, such ashuman
,singular
, and others that are inflected from the model name. -
ActiveModel::Translation: Provides integration between your model and Rails Internationalization (i18n) framework.
-
ActiveModel::Validations: Provides a full validation framework to your objects.
- acceptance
- validates_associated
- confirmation
- exclusion
- format
- inclusion
- length
- numericality
- presence
- absence
- uniqueness
- validates_with
- validates_each
def validates_presence_of( *attr_names )
validates_with PresenceValidator, _merge_attributes(attr_names)
end
-
ActiveModel::Callbacks: Provide an interface for any class to have Active Record like callbacks.
-
ActiveModel::Dirty: Provides a way to track changes in your object in the same way as Active Record does.
-
ActiveModel::Serialization: Provides a basic serialization to a
#serializable_hash
for your objects.
Used Ruby autoload that allow us lazily load a constant when it is first referenced and a fast boot process, example:
module Example
autoload :Base, "example/base"
end
Rails provides a module called ActiveModel::Lint::Tests that defines several tests asserting that each method required in an Active Model--compliant API exist.
Rails provides hooks that allow us to retriece templates from anywhere. Whenever we render a template in Rails, its source must first be compiled into executable Ruby code. Every time some Ruby code is executed, its execution happens inside a given context and, in a Rails application, views are executed inside the view context object.
The view renderer has access to an instance of ActionView::LookupContext usually referred to as lookup_context. The lookup context is shared between controllers and views. It`s also responsible for holding all view paths. A view path is a collection of objects able to find templates.
Rails provides a well-defined API for adding any object as a view path. This means we’re not forced to store view templates in the filesystem.
Internally Rails calls them template resolvers (ActionView::Resolver) and they must comply with the Resolver API.
The Resolver API is composed of a single method: find_all
which should return an array of templates.
Handler is anything that responds to call and returns a String.
The template handler`s responsibility in the rendering stack is to compile a template to Ruby source code.
ActionView::Template.register_template_handler(extension, handler)
require "action_view/template"
ActionView::Template.register_template_handler :rb, lambda { |template| template.source }
For an object to be compliant with de handler API it needs to respond to call
method.
Rails generators provide hooks to allow other generatos to extend ans customize the generated code:
- Controller generator: Erb::Generators::ControllerGenerator;
- Mailer generator: Erb::Generators::MailerGenerator;
- Scaffold generator: Erb::Generators::ScaffoldGenerator;
Erb::Generators::MailerGenerator inherits from Erb::Genearats::ControllerGenerator and use format :text
.
Take a look Thor::Actions.
Rails::Railtie allow us to hook into Rails`s and configure some defaults.You should include a railtie in your plug-in only if at least one of the following is true:
- Your plug-in needs to perform a given task while or after the Rails application is initialized;
- Your plug-in needs to change a configuration value for instance, setting a generator;
- Your plug-in must provide Rake tasks and generators in nondefaults locations;
- Your plug-in wants to run custom code whenever the Rails console or the Rails runner is started;
- You want your plug-in to provide configuration options to the applciation, config.my_plugin.key = :value;
Extending Rails with Engines
Rails::Engine is a Rails::Railtie with some default initializers and the Paths application programming interface (API). An Engine has several initializers that are responsible for booting the engine. Stream events using ActionController::Live. The streaming functionality doesn’t work in WEBrick, example using Puma.
Polling was the most common technique to receive updates from the server in the browser (a lot of requests generating a lot of over-head).
WebSockets: allow both the client and serve to exchange information over the same connection (new protocol) Server Sent Events (SSE): is a one way communication channel from the server to the client, and can be used with any web server that is able to stream responses.
Used the gem listen that exposes all major operation-system notifications mechanisms under a single, easy-to-use API.
Threads Queues
Code-loading techniques
Autoload techniques:
- Rails`s autoloading = ActiveSupport::Dependencies
- Ruby`s autoload thread-safe
- Rails`s autoload isn’t thread-safe
Rails allow only one thread to run by default, which means it can serve only one request at a time.
config.allow_concurrency (our own risk)
Eager-Load Techniques:
Rails eager-load your code inside app folder on boot.
config.eager_load_namespaces #Keeps a list of namespaces to eager-load
Keep in mid that eager loading is not only beneficial for threaded web servers like Puma, but also for fork-based servers like Unicorn.
We guarantee that all our code code loaded up front and we don’t need to spend time autoloading the code on every request.
Scaffolding the only problem with scaffolding is that generated controllers are still a little bit verbose.
To avoid a lot of similar blocks respond_to
at controllers, Rails provide respond_with
that uses ActionController::Responder to abstract how controllers respond.
We need to understand the three variables that affect how controllers respond: request type, HTTP verb and resource status.
Navigational and API Requests: HTML and JavaScript Object Notation (JSON).
Anything that responds to call and accept three arguments (controller, resource and hash options) can be a responder.
navigation_behavior
(html) and api_behavior
(xml, json)
The great advantage in using ActionController::Responder is that it centralizes how our application should behave per format.
Set a Responder:
ApplicationController.responder = MyAppResponder
or custom responder per controller:
class MyController < ApplicationController
self.responder = MyCustomResponder
end
Implemented a Flash Responder (Responders::Flash)
Implemented a HTTP Cache Responder (Responders::HttpCache)
Rails provides several helpers on top of HTTP cache specification:
response.last_modified
request.fresh?(response)
head
get?
ActionController::Base.perform_caching
Customize Generators Responders::Generators::InstallGenerator
The controller generated by scaffolding should change depending on the ORM. Rails solves this problem by creating an object responsible for telling the scaffold generator how the interaction with the ORM happens (Rails::Generators::ActiveModel).
Rails provides a centralized way to publish and subscribe to events happening inside an application with the ActiveSupport::Notifications application programming interface (API).
Used this API to subscribe to all actions processes by application ans store them in MongoDB database.
Mountable and isolated engines:
A mountable engine uses its own router instead of adding routes directly to the application router. An isolated engine is built inside its own namespace, with its own models, controllers, views, assets and helpers.
rails plugin new my_plugin --mountable
Routes:
Rails.application.routes.draw do
mount MyPlugin::Engine => 'my_plugin'
Isolated namespace:
module MyPlugin
class Engine < ::Rails::Engine
isolate_namespace MyPLugin
end
end
The Notifications API consists of just two methods: instrument
and subscribe
ActiveSupport::Notifications.instrument("process_action.action_controller", format: :html, path: "/", action: "index") do
process_action("index")
end
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
# do something
end
args:
- name
- started_at
- ended_at
- instrumenter_id
- payload
Rails and Rack
Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby.
We can easily mount a Sinatra application inside the Rails router, similar to how we mounted an engine inside the dummy application.
A Rack application is any Ruby object that responds to call. It takes exactly one argument, the environment, and returns an array of exactly three values: the status, the headers, and the body:
# config.ru
# $ rackup -s puma
require 'rack'
class StreamingRack
def call(env)
[200, { 'Content-Type' => 'text/html' }, self]
end
def each
while true
yield 'Helo Rack!\n'
end
end
end
run StreamingRack.new
Whenever you call get, post, put, delete, resources, or resource in the router domain-specific language, those methods invoke to the match method. The only method that has different semantics is mount, used in the dummy application to mount our engine.
Rack provides the concept of middleware, which allows us to add custom code between those Rack applications, giving us even more flexibility.
A middleware wraps around a Rack application. It lets us manipulate both the request sent down to the application and the response the application returns. Any request to an action in a Rails controller passes through three middleware stacks.
Rails::Rack::LogTailer Rails::Rack::Debugger
We can add midlleware to a controller:
class MyController < ApplicationController
use MyMiddleware
end
I18n framework comes with diferrent back ends that allow us to store translations in places other tahn YAML files. On the downside, retrieving translations from the database instead of an in-memory hash has a huge impact on performance.
Rails::Application specific behavior:
- An application is responsible for all bootstrapping;
- An application has its own router and middleware stack;
- An application should load an initialize all plug-ins;
- An application is responsible for reloading code and routes between requests if the changed;
- An application is responsible for loading tasks and generators when appropriate;
All initializers available in a Rails application:
Rails.application.initializers.map(&:name)
Some routes
may be defined during initialization or inside a file that is nerver reloaded, so Rails provide routes.prepend
a and routes.append
to define sticky routes.
The I18n framework ships with three different back ends:
- I18n::Backend::Simple: Keeps translations in an in-memory hash populated from YAML files;
- I18n::Backend::KeyValue: Uses any key-value store as a back end;
- I18n::Backend::Chain: Allows you to chain several back ends; in other words, if a translation cannot be found in one back end, it searches for it in the next back end in the chain;
I18n Transliteration support:
I18n.transliterate("dziękuję") # => "dziekuje"
Extensions provided by the I18n library:
- I18n::Backend::Cache
- I18n::Backend::Cascade
- I18n::Backend::Fallbacks
- I18n::Backend::Gettext
- I18n::Backend::InterpolationCompiler
- I18n::Backend::Memoize
- I18n::Backend::Metadata
- I18n::Backend::Pluralization
- I18n::Backend::Transliterator
Integrate Rails + Sinatra + Haml template markup (Mount Sinatra app inside Rails application with specific route)
Add Devise a full-stack authentication solution based on Rack, and take a deeper look at how we can use Capybara to test Rack applications.
Add authentication to Sinattra using a proxy Warden middleware:
env["warden"].authenticate!(scope: "admin")