Extend Inherited Resources actions with an awesome package of extensions.
To specify request parameter-driven filtering, sorting, pagination, etc. with defaults, just use extensions
:
class PostsController < ApplicationController
inherit_resources
actions :index
extensions :count, :distinct, :limit, :offset, :paging
respond_to :json, :html
can_filter_by :author, through: {author: :name}
default_filter_by :author, eq: 'anonymous'
can_filter_by :posted_on, using: [:lt, :eq, :gt]
default_filter_by :posted_on, gt: 1.year.ago
can_filter_by :company, through: {author: {company: :name}
can_order_by :posted_on, :author, :id
default_order_by {:posted_on => :desc}, :id
end
Then set up your routes and views.
Now here are some of the URLs you can hit:
https://example.org/posts?author=John
https://example.org/posts?posted_on.gt=2012-08-08
https://example.org/posts?posted_on.gt=2012-08-08&count=
https://example.org/posts?company=Lipton
https://example.org/posts?page_count=
https://example.org/posts?page=1
https://example.org/posts?offset=30&limit=15
https://example.org/posts?order=author,-id
You can also define a query to allow only admins to see private posts:
index_query ->(q) { @current_user.admin? ? q : q.where(:access => 'public') }
and change the query depending on a supplied param:
can_filter_by_query \
status: ->(q, status) {
status == 'all' ? q : q.where(:status => status)
},
color: ->(q, color) {
if color == 'red'
q.where("color = 'red' or color = 'ruby'")
else
q.where(:color => color)
end
}
A comparison with Inherited Resources:
- Irie was developed to solve the problem of needing a flexible JSON service controller for applications intended to be used with JavaScript frameworks such as AngularJS and Ember, but evolved into a set of concerns you can include to build your controller easily.
- Inherited Resources was primarily developed for handling HTML forms in Rails, but supports other formats as well.
- Irie supports flexible request param based filtering and ordering, both with defaults, quick to write lambda queries and filters, and a relational param syntax.
- Inherited Resources has some finder options by request param value, and relies more heavily on fat models to handle behavior.
In your Rails app's Gemfile
:
gem 'irie'
Then:
bundle install
Each application-level configuration option can be configured one line at a time:
Irie.number_of_records_in_a_page = 30
or in bulk, like:
Irie.configure do
# Default for :using in can_filter_by.
self.can_filter_by_default_using = [:eq]
# Delimiter for values in request parameter values.
self.filter_split = ','
# Use one or more alternate request parameter names for functions, e.g.
# `self.function_param_names = {distinct: :very_distinct, limit: [:limit, :limita]}`
self.function_param_names = {}
# Delimiter for ARel predicate in the request parameter name.
self.predicate_prefix = '.'
# You'd set this to false if id is used for something else other than primary key.
self.id_is_primary_key_param = true
# Used when paging is enabled.
self.number_of_records_in_a_page = 15
# When you include the defined action module, it includes the associated modules.
# If value or value array contains symbol it will look up symbol in
# Irie.available_extensions in the controller (which is defaulted to
# `::Irie.available_extensions`). If value is String will assume String is the
# fully-qualified module name to include, e.g. `index: '::My::Module'`, If constant,
# it will just include constant (module), e.g. `index: ::My::Module`.
self.autoincludes = {
create: [:query_includes, :render_options, :resource_path_and_url],
destroy: [:query_includes, :render_options, :resource_path_and_url],
edit: [:query_includes, :render_options, :edit_path_and_url],
index: [:index_query, :order, :param_filters, :query_filter, :query_includes, :collection_path_and_url],
new: [:new_path_and_url],
show: [:query_includes, :resource_path_and_url],
update: [:query_includes, :render_options, :resource_path_and_url]
}
# By default, it sets the instance variable, but does not return entity if request
# update, e.g. in JSON format.
self.update_should_return_entity = false
end
You may want to put your configuration in an initializer like config/initializers/irie.rb
.
The default controller config may be fine, but you can customize it.
In the controller, you can set a variety of class attributes with self.something = ...
in the body of your controller.
All of the app-level configuration parameters are configurable in the controller class body, e.g.:
self.can_filter_by_default_using = [:eq]
self.filter_split = ','
self.function_param_names = {}
self.predicate_prefix = '.'
self.number_of_records_in_a_page = 15
self.id_is_primary_key_param = true
self.update_should_return_entity = false
Though it will try to determine them implicitly from the controller name, if you need to override one or more pieces of info about the model, you can do that from a defaults
method which is partially-similar to the one in inherited_resources:
defaults resource_class: Barfoo, instance_name: 'barfoo', collection_name: 'barfoos'
As you may have noticed in autoincludes
, some concerns are included as a package along with the action include.
The following assumes that you are using the default autoincludes and included the relevant action.
can_filter_by
filters the index action the request parameter name as a symbol will filter the results by the value of that request parameter, e.g.:
can_filter_by :title
allows you to filter by title:
http://localhost:3000/posts?title=Awesome
If you do Arel::Predications.public_instance_methods.sort
in Rails console, you can see a list of the available predicates:
:does_not_match, :does_not_match_all, :does_not_match_any, :eq, :eq_all, :eq_any, :gt,
:gt_all, :gt_any, :gteq, :gteq_all, :gteq_any, :in, :in_all, :in_any, :lt, :lt_all,
:lt_any, :lteq, :lteq_all, :lteq_any, :matches, :matches_all, :matches_any, :not_eq,
:not_eq_all, :not_eq_any, :not_in, :not_in_all, :not_in_any
:using
means you can use those ARel predicates for filtering:
can_filter_by :seen_on, using: [:gteq, :eq_any]
By appending the predicate prefix (.
by default) to the request parameter name, you can use any ARel predicate you allowed, e.g.:
http://localhost:3000/posts?seen_on.gteq=2012-08-08
And can_filter_by
can specify a :through
which (inner) joins and sets the deepest symbol in the hash as the key for the parameter value, then does a where, e.g.:
can_filter_by :name, through: {company: {employee: :full_name}}
If a MagicalUnicorn has_many :friends
and a MagicalUnicorn's friend has a name attribute:
can_filter_by :magical_unicorn_friend_name,
through: {magical_unicorns:{friends: :name}}
and use this to get valleys associated with unicorns who in turn have a friend named Oscar:
http://localhost:3000/magical_valleys?magical_unicorn_friend_name=Oscar
Use can_filter_by_query
to provide a lambda:
can_filter_by_query a_request_param_name: ->(q, param_value) {
q.joins(:some_assoc).where(some_assocs_table_name: {some_attr: param_value})
}
The second argument sent to the lambda is the request parameter value converted by the convert_param_value(param_name, param_value)
method which may be customized. See elsewhere in this document for more information about the behavior of this method.
The return value of the lambda becomes the new query, so you could really change the behavior of the query depending on the request parameter provided.
Implement the convert_param_value(param_name, param_value)
in your controller or an included module.
Specify default filters to define attributes, ARel predicates, and values to use if no filter is provided by the client with the same param name, e.g. if you have:
can_filter_by :attr_name_1
can_filter_by :production_date, :creation_date, using: [:gt, :eq, :lteq]
default_filter_by :attr_name_1, eq: 5
default_filter_by :production_date, :creation_date, gt: 1.year.ago,
lteq: 1.year.from_now
and both attr_name_1 and production_date are supplied by the client, then it would filter by the client's attr_name_1 and production_date and filter creation_date by both > 1 year ago and <= 1 year from now.
In the controller:
includes_extension :distinct
enables:
http://localhost:3000/posts?distinct=
In the controller:
include_extension :count
enables:
http://localhost:3000/posts?count=
That will set the @count
instance variable that you can use in your view.
Use include_extension :autorender_count
to render count automatically for non-HTML (JSON, etc.) views.
In the controller:
include_extension :paging
enables:
http://localhost:3000/posts?page_count=
That will set the @page_count
instance variable that you can use in your view.
Use include_extension :autorender_page_count
to render count automatically for non-HTML (JSON, etc.) views.
In the controller:
include_extension :paging
To access each page of results:
http://localhost:3000/posts?page=1
http://localhost:3000/posts?page=2
To set page size at application level:
Irie.number_of_records_in_a_page = 15
To set page size at controller level:
self.number_of_records_in_a_page = 15
In the controller:
include_extensions :offset, :limit
enables:
http://localhost:3000/posts?offset=5
http://localhost:3000/posts?limit=5
You can combine them to act like page:
http://localhost:3000/posts?limit=15
http://localhost:3000/posts?offset=15&limit=15
http://localhost:3000/posts?offset=30&limit=15
You can allow request specified order:
can_order_by :foo_date, :foo_color
Will let the client send the order parameter with those parameters and optional +/- prefix to designate sort direction, e.g. the following will sort by foo_date ascending then foo_color descending:
http://localhost:3000/posts?order=foo_date,-foo_color
The default_order_by
specifies an ordered array of ascending attributes and/or hashes of attributes to sort direction:
default_order_by :posted_at => :desc, :id => :desc
or:
default_order_by {:this_is_desc => :desc}, :this_is_asc,
{:no_different_than_a_symbol => :asc},
:this_is_asc_also, :id => :desc
To filter the list where the status_code attribute is 'green':
index_query ->(q) { q.where(:status_code => 'green') }
You can also filter out items that have associations that don't have a certain attribute value (or anything else you can think up with ARel/ActiveRecord relations), e.g. to filter the list where the object's apples and pears associations are green:
index_query ->(q) {
q.joins(:apples, :pears)
.where(apples: {color: 'green'})
.where(pears: {color: 'green'})
}
To avoid n+1 queries, use .includes(...)
in your query to eager load any associations that you will need in the JSON view.
If you wanted to specify the serializer option in the index:
render_options :index, serializer: PostSerializer
(You can use more than one action and more than one option.)
Also available are render_valid_options
and render_invalid_options
(when record/collection respond to .errors
and has more than one error) that are merged into any render_options
you provide. (Please see the Exception Handling section for information about handling exceptions.)
For more control, you can either implement options_for_render(record_or_collection)
, or both options_for_collection_render(records)
for index, and options_for_render(record)
for other actions. Or, implement any action's render_*(...)
method (where * is the action name).
# load all the posts and the associated category and comments for each post
query_includes :category, :comments
or
# load all of the associated posts, the associated posts’ tags and comments, and every comment’s guest association
query_includes posts: [{comments: :guest}, :tags]
and action-specific:
query_includes_for :create, are: [:category, :comments]
query_includes_for :index, :show, are: [posts: [{comments: :guest}, :tags]]
Each Irie-implemented action method except new
calls a corresponding params_for_*
method. For create
and update
this calls (model_name)_params
method expecting you to have defined that method to call permit
, e.g.
def post_params
params.require(:post).permit(:name)
end
But, if you need action-specific permittance, just override the corresponding params_for_*
method, e.g. if you'd like to override the params permittance for both create and update actions, you can implement the params_for_create
and params_for_update
methods, and you won't even need to implement a (model_name)_params
, since those two method are what call that:
def params_for_create
params.require(:post).permit(:name, :color)
end
def params_for_update
params.require(:post).permit(:color)
end
The :through
option in can_filter_by
and can_order_by
just uses define_params
to set the attribute name alias and options (which is parsed into a joins hash and attribute name internally). So, if you don't mind a little more typing, it might make the intent clearer, e.g.
define_params name: {company: {employee: :full_name}},
color: :external_color
can_filter_by :name
default_filter_by :name, eq: 'Guest'
can_order_by :color
default_filter_by :color, eq: 'blue'
You may or may not need these, but each of the following are autoincluded with the relevant actions in the default configuration.
:collection_path_and_url
- implements collection name path and url methods on include and revised values when subclass from class that includes.:edit_path_and_url
- implements edit resource name path and url on include and revised values when subclass from class that includes.:new_path_and_url
- implements new resource name path and url on include and revised values when subclass from class that includes.:resource_path_and_url
- implements resource name path and url on include and revised values when subclass from class that includes.
Path and URL methods should "just work" if you follow the Rails convention of the controller name being the plural form of the model followed by "Controller", e.g. Namespace::Does::Not::Matter::Here::FoosController
manages model Foo
. If you must a different collection name/resource name/model class name, be sure to call resource_definition_updated
which should update these.
The following concerns, which you can include via include_extension ...
or via including the corresponding module, might also be of use in your controller:
:authorizing
- integrates with CanCan-compatible authorizer.:nil_params
- convert 'NULL', 'null', and 'nil' to nil when passed in as request params.:autorender_errors
- renders validation errors (e.g.@my_model.errors
) for non-HTML (JSON, etc.) formats without a view template.:rfc2616
- use RFC 2616 RESTful status codes for create and update.
Extensions are just modules. There is no magic.
The somewhat special thing about Irie extensions if that you can @action_result = ...; throw(:action_break)
in any method that is called by an Irie action and it will break the execution of the action and return @action_result
. This allows the ease of control that you'd have typically in a single long action method, but lets you use modules to easily share action method functionality. To those unfamiliar, throw
in Ruby is a normal flow control mechanism, unlike raise
which is for exceptions.
Some hopefully good examples of how to extend modules are in lib/irie/extensions/* and the actions themselves are in lib/irie/actions/*. Get familiar with the code even if you don't plan on customizing, if for no other reason than to have another set of eyes on the code.
Here's another example:
# Converts all 'true' and 'false' param values to true and false
module BooleanParams
extend ::ActiveSupport::Concern
def convert_param_value(param_name, param_value)
case param_value
when 'true'
true
when 'false'
false
else
super if defined?(super)
end
end
end
If you are just doing regular include
's in your controllers, that's all you need. If you'd like to use the include_action
/include_extension
method of doing includes, which lets you include bundles of includes, a good place to register the custom concerns is in a shared module, e.g.
in app/controllers/concerns/service_controller.rb
:
module ServiceController
extend ::ActiveSupport::Concern
included do
include ::Irie::Controller
respond_to :json
# note: Referencing as string so we don't load the concern before it is used.
::Irie.available_actions[:my_custom_action] = '::MyCustomAction'
::Irie.available_extensions[:boolean_params] = '::BooleanParams'
end
end
Now you could use this in your controller:
include ServiceController
include_action :my_custom_action
include_extension :boolean_params
Doing that doesn't make as much sense when you just have modules in the root namespace, but it might if you have longer namespaces for organization and to avoid class/module name conflicts.
Supports composite primary keys. If resource_class.primary_key.is_a?(Array)
, show/edit/update/destroy will use your two or more request params for the ids that make up the composite.
Rails 4 has basic exception handling in the public_exceptions and show_exceptions Rack middleware.
If you want to customize Rails 4's Rack exception handling, search the web for customizing config.exceptions_app
, although the default behavior should work for most.
You can also use rescue_from
or around_action
in Rails to have more control over error rendering.
If you get missing FROM-clause entry for table
errors, it might mean that query_includes
/query_includes_for
you are using are overlapping with joins that are being done in the query. This is the nasty head of AR relational includes, unfortunately.
To fix, you may decide to either: (1) change order/definition of includes in query_includes
/query_includes_for
, (2) don't use query_includes
/query_includes_for
for the actions it affects (may cause n+1 queries), (3) implement apply_includes
to do includes in an appropriate order (messy), or (4) use custom query (if index/custom list action) to define joins with handcoded SQL, e.g. (thanks to Tommy):
index_query ->(q) {
# Using standard joins performs an INNER JOIN like we want, but doesn't
# eager load.
# Using includes does an eager load, but does a LEFT OUTER JOIN, which
# isn't really what we want, but in this scenario is probably ok.
# Using standard joins & includes results in bad SQL with table aliases.
# So, using includes & custom joins seems like a decent solution.
q.includes(:bartender, :waitress, :owner, :customer)
.joins('INNER JOIN employees bartenders ON bartenders.employee_id = ' +
'shifts.bartender_id')
.joins('INNER JOIN waitresses shift_workers ON shift_workers.id = ' +
'shifts.waitress_id')
.where(bartenders: {certified: 'yes'})
.where(shift_workers: {attitude: 'great'})
}
# set includes for all actions except index
query_includes :owner, :customer, :bartender, :waitress
# includes specified in index query
query_includes_for :index, are: []
If you enabled Irie's debug option via:
Irie.debug = true
Then all the included modules (actions, extensions) will use logger.debug ...
to log some information about what is executed.
To log debug to console only in your tests, you could put this in your test helper:
::Irie.debug = true
ActionController::Base.logger = Logger.new(STDOUT)
ActionController::Base.logger.level = Logger::DEBUG
However, that might not catch all the initialization debug logging that could occur. Instead, you might put the following into the block in config/environments/test.rb
:
::Irie.debug = true
config.log_level = :debug
If there is a problem with class load related to includes, add this to the class body of your controller:
require 'irie/controller_debugging'
extend ::Irie::ControllerDebugging
ActionController::Base.logger.level = Logger::DEBUG
# Ensure this comes after all relevant includes.
output_irie_debugging_info
If there is a problem with instance methods/behavior related to includes, add this to the class body of your controller:
require 'irie/controller_debugging'
include ::Irie::ControllerDebugging
ActionController::Base.logger.level = Logger::DEBUG
def initialize(*args)
output_irie_debugging_info
end
That will output all include registration information including the ordered controller's registered includes and method inheritance order for registered includes.
The project was originally named restful_json, but was renamed to Irie when it no longer was meant to be JSON-service specific. If you need the old tags, they can be found in legacy.
See changelog and git log.
Please fork, make changes in a separate branch, and do a pull request. Thanks!
This was written by FineLine Prototyping, Inc. by the following contributors:
Copyright (c) 2013 FineLine Prototyping, Inc., released under the MIT license.