Inherited Resources including extensions. Tested with Rails 3.2, 4.0, 4.1, Rails edge, Inherited Resources 1.4 and edge, in Ruby 1.9.3, 2.0.0, and jruby-19mode.
Extend Inherited Resources actions with the extensions
method which provides symbolic references to do module includes as well as automatic inclusion of modules based on what actions
are defined. The included extensions provide more of a DSL-like way to define your controllers. And, instead of model-heavy development via scope
in models and has_scope
in the controller, you can just define request parameter-based filters and their defaults in the controller. Ordering, parameter value conversion, pagination, and more are supported without additional dependencies.
class PostsController < ApplicationController
respond_to :json
inherit_resources
actions :index
extensions :count, :limit, :offset, :paging
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 any 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
}
Note: extensions
also automatically includes common sets of extensions with certain actions. So, just specify extensions
by itself can include things you can use, e.g.
class PostsController < ApplicationController
inherit_resources
actions :index
extensions
end
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]
# 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
# Included if the action method exists when `extensions` is called.
self.autoincludes = {
create: [:smart_layout, :query_includes],
destroy: [:smart_layout, :query_includes],
edit: [:smart_layout, :query_includes],
index: [:smart_layout, :index_query, :order, :param_filters, :params_to_joins, :query_filter, :query_includes],
new: [:smart_layout],
show: [:smart_layout, :query_includes],
update: [:smart_layout, :query_includes]
}
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.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
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.
Inherited Resources has has_scope
via the has_scope dependency, which is still available for use, and is very powerful. But, Irie also has can_filter_by
. It is an alternative, not a replacement.
Unlike has_scope
, can_filter_by
doesn't require a scope
on the model and has_scope
on the controller. Instead you just have a single can_filter_by
in the controller rather. Other differences are that the request parameter syntax is more brief, and it adds support for defining deeply associated attributes via either define_params
or a through
option.
Like the combination of scope
and has_scope
, 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. assuming :eq
is in the configured can_filter_by_default_using
in your config, as it is by default:
can_filter_by :title
allows you to:
http://localhost:3000/posts?title=Awesome
or
http://localhost:3000/posts?title.eq=Awesome
Since .eq
is optional in the param name.
And, like has_scope
, predications are supported. Do Arel::Predications.public_instance_methods.sort
in Rails console to see the list:
: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
You can specify these via the using:
option:
can_filter_by :seen_on, using: [:gteq, :eq_any]
For predicates that take more than one value, by default it expects that you send in multiple request parameters, that way if a value contains something that would be a delimiter of the value, you don't have to worry about additional escaping characters in the value to what you'd have to do otherwise. But, if a value is numeric, for example, you might want to be able to specify a comma-delimited list, and you can do so via:
can_filter_by :mileage, using: [:eq_any], split: ","
Unlike has_scope
which uses a much lengthier request parameter syntax, 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
supports (inner) joins created by define_params
or if you'd rather, you 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
Similar to specifying a proc/lambda in the scope
and then using has_scope
to use it, or defining a scope
in the model and defining has_scope
and passing a block into it, you can use can_filter_by_query
, but again you only have to define something in the controller- not the model and controller. It works a little bit differently; the proc/lambda is in the context of the controller, so unlike the has_scope
that takes a block, the controller
doesn't have to be passed in, since that is self
. The relation object is passed in as q
, e.g.:
can_filter_by_query a_request_param_name: ->(q, param_value_or_values) {
q.joins(:some_assoc).where(some_assocs_table_name: {some_attr: param_value_or_values})
}
The second argument sent to the lambda (param_value_or_values
) is the request parameter value converted by the convert_param(param_name, param_values)
method, which may be customized through included extensions or your own extension. 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(param_name, param_values)
in your controller or an included module. See Writing Your Own Extensions for an example.
Like the combination of scope
and has_scope
available via has_scope, which you can still use, defaults are supported that are compatible with can_filter_by
.
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:
extensions :count
enables:
http://localhost:3000/posts?count=
That will set the @count
instance variable that you can use in your view.
Use extensions :autorender_count
to render count automatically for non-HTML (JSON, etc.) views.
In the controller:
extensions :paging
enables:
http://localhost:3000/posts?page_count=
That will set the @page_count
instance variable that you can use in your view.
Use extensions :autorender_page_count
to render count automatically for non-HTML (JSON, etc.) views.
In the controller:
extensions :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:
extensionss :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
can_order_by
and default_order_by
support joins/names_params
as well as a through
option on can_order_by
similar to can_filter_by
.
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.
By default, Rails rendering goes through some extra hoops to attempt to find your layout unless you tell it not to, so With default autoincludes, Irie will use the :smart_layout
extension to specify layout: false
unless the request format is html.
# 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]]
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'
The following concerns, which you can include via extensions ...
or via including the corresponding module, might also be of use in your controller:
:nil_params
- convert 'NULL', 'null', and 'nil' to nil when passed in as request params.
Extensions are just modules. There is no magic.
Some hopefully good examples of how to extend modules are in this project in lib/irie/extensions/
.
Here's a quick example:
module Example
module BooleanParams
extend ::ActiveSupport::Concern
TRUE_VALUE = 'true'.freeze
FALSE_VALUE = 'false'.freeze
protected
# Converts request param value(s) 'true' to true and 'false' to false
def convert_param(param_name, param_value_or_values)
logger.debug("Example::BooleanParams.convert_param(#{param_name.inspect}, #{param_value_or_values.inspect})") if ::Irie.debug?
param_value_or_values = super if defined?(super)
if param_value_or_values.is_a? Array
param_value_or_values.map {|v| convert_boolean(v)}
else
convert_boolean(param_value_or_values)
end
end
private
def convert_boolean(value)
case value
when TRUE_VALUE
true
when FALSE_VALUE
false
else
value
end
end
end
end
If you are just doing regular include
's in your controllers, that's all you need, and you can include when you need to.
If you'd like to use your modules via the extensions
method, just register the extension in an initializer, e.g. in config/initializers/irie.rb
:
# note: Referencing as string so we don't load the concern before it is used.
::Irie.register_extension :boolean_params, '::Example::BooleanParams'
Now, you could do this in your controller:
respond_to :json
inherit_resources
actions :index
extensions :boolean_params
Irie includes a way to specify order of module inclusion independent of the class/module it is included in, and you can specify that at registration, e.g. in an initializer like config/initializers/irie.rb
, you might do one of the following:
::Irie.register_extension :boolean_params, '::Example::BooleanParams', include: :last # last is the default, so don't need to specify this option
::Irie.register_extension :boolean_params, '::Example::BooleanParams', include: :first
::Irie.register_extension :boolean_params, '::Example::BooleanParams', after: :nil_params
::Irie.register_extension :boolean_params, '::Example::BooleanParams', before: :nil_params
Note: an extension must be registered before you can use after:
or before:
to place your extension include after or before it. Use require
and declare dependencies if possible to ensure registration of other extensions, and nothing is stopping you from registering something else. The extension class constant isn't going to be referenced by Irie until extensions
is called with it.
The extensions
method is just a companion to actions
if you want to use it. You can still use include/extend/prepend, if you'd rather.
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
The project was originally named restful_json. Old commit tags corresponding to restful_json versions may 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.