irie 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +657 -0
- data/Rakefile +21 -0
- data/lib/irie.rb +23 -0
- data/lib/irie/class_methods.rb +61 -0
- data/lib/irie/config.rb +131 -0
- data/lib/irie/configuration_error.rb +4 -0
- data/lib/irie/extensions/autorender_count.rb +17 -0
- data/lib/irie/extensions/conversion/nil_params.rb +27 -0
- data/lib/irie/extensions/count.rb +21 -0
- data/lib/irie/extensions/index_query.rb +48 -0
- data/lib/irie/extensions/limit.rb +25 -0
- data/lib/irie/extensions/offset.rb +25 -0
- data/lib/irie/extensions/order.rb +153 -0
- data/lib/irie/extensions/paging.rb +43 -0
- data/lib/irie/extensions/paging/autorender_page_count.rb +19 -0
- data/lib/irie/extensions/param_filters.rb +137 -0
- data/lib/irie/extensions/params_to_joins.rb +108 -0
- data/lib/irie/extensions/query_filter.rb +58 -0
- data/lib/irie/extensions/query_includes.rb +107 -0
- data/lib/irie/extensions/smart_layout.rb +20 -0
- data/lib/irie/param_aliases.rb +36 -0
- data/lib/irie/version.rb +3 -0
- metadata +124 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 70d0c4b896c361b3118094166c14bae9a734a530
|
4
|
+
data.tar.gz: ea3ff0813dabc51ba793191d92797f8c9f2b6704
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9733f3a4277253428161f1c4b735832fbd5f53bf958b48236a90ec2a5249d143fe476d15814536126d3a690e47ef9fc5c53e6271af89dca314069837072acb62
|
7
|
+
data.tar.gz: c4b1a88c2571906e7378ee09a76409f2669dcd41e5402da6b55927881dfd33a9baf4e9bb248e6cbedd9dea7de921e05cceac16a6e0f56b17e8ac0ce9e4cdf7ba
|
data/README.md
ADDED
@@ -0,0 +1,657 @@
|
|
1
|
+
[][travis] [][badgefury]
|
2
|
+
|
3
|
+
# Irie
|
4
|
+
|
5
|
+
Inherited Resources including extensions. Tested with Rails 4/edge, Inherited Resources 1.4/edge in Ruby 1.9.3, 2.0.0, and jruby-19mode.
|
6
|
+
|
7
|
+
Extend [Inherited Resources][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 in-use. 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. Also, ordering, parameter conversion, param value split delimiters, pagination, and more are supported.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class PostsController < ApplicationController
|
11
|
+
|
12
|
+
respond_to :json
|
13
|
+
inherit_resources
|
14
|
+
|
15
|
+
actions :index
|
16
|
+
extensions :count, :limit, :offset, :paging
|
17
|
+
|
18
|
+
can_filter_by :author, through: {author: :name}
|
19
|
+
default_filter_by :author, eq: 'anonymous'
|
20
|
+
|
21
|
+
can_filter_by :posted_on, using: [:lt, :eq, :gt]
|
22
|
+
default_filter_by :posted_on, gt: 1.year.ago
|
23
|
+
|
24
|
+
can_filter_by :company, through: {author: {company: :name}
|
25
|
+
|
26
|
+
can_order_by :posted_on, :author, :id
|
27
|
+
default_order_by {:posted_on => :desc}, :id
|
28
|
+
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
Then set up your routes and any views.
|
33
|
+
|
34
|
+
Now here are some of the URLs you can hit:
|
35
|
+
|
36
|
+
```
|
37
|
+
https://example.org/posts?author=John
|
38
|
+
https://example.org/posts?posted_on.gt=2012-08-08
|
39
|
+
https://example.org/posts?posted_on.gt=2012-08-08&count=
|
40
|
+
https://example.org/posts?company=Lipton
|
41
|
+
https://example.org/posts?page_count=
|
42
|
+
https://example.org/posts?page=1
|
43
|
+
https://example.org/posts?offset=30&limit=15
|
44
|
+
https://example.org/posts?order=author,-id
|
45
|
+
```
|
46
|
+
|
47
|
+
You can also define a query to allow only admins to see private posts:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
index_query ->(q) { @current_user.admin? ? q : q.where(:access => 'public') }
|
51
|
+
```
|
52
|
+
|
53
|
+
and change the query depending on a supplied param:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
can_filter_by_query \
|
57
|
+
status: ->(q, status) {
|
58
|
+
status == 'all' ? q : q.where(:status => status)
|
59
|
+
},
|
60
|
+
color: ->(q, color) {
|
61
|
+
if color == 'red'
|
62
|
+
q.where("color = 'red' or color = 'ruby'")
|
63
|
+
else
|
64
|
+
q.where(:color => color)
|
65
|
+
end
|
66
|
+
}
|
67
|
+
```
|
68
|
+
|
69
|
+
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.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class PostsController < ApplicationController
|
73
|
+
inherit_resources
|
74
|
+
|
75
|
+
actions :index
|
76
|
+
extensions
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
### Installation
|
81
|
+
|
82
|
+
In your Rails app's `Gemfile`:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
gem 'irie'
|
86
|
+
```
|
87
|
+
|
88
|
+
Then:
|
89
|
+
|
90
|
+
|
91
|
+
```
|
92
|
+
bundle install
|
93
|
+
```
|
94
|
+
|
95
|
+
### Application Configuration
|
96
|
+
|
97
|
+
Each application-level configuration option can be configured one line at a time:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
::Irie.number_of_records_in_a_page = 30
|
101
|
+
```
|
102
|
+
|
103
|
+
or in bulk, like:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
::Irie.configure do
|
107
|
+
|
108
|
+
# Default for :using in can_filter_by.
|
109
|
+
self.can_filter_by_default_using = [:eq]
|
110
|
+
|
111
|
+
# Use one or more alternate request parameter names for functions, e.g.
|
112
|
+
# `self.function_param_names = {distinct: :very_distinct, limit: [:limit, :limita]}`
|
113
|
+
self.function_param_names = {}
|
114
|
+
|
115
|
+
# Delimiter for ARel predicate in the request parameter name.
|
116
|
+
self.predicate_prefix = '.'
|
117
|
+
|
118
|
+
# You'd set this to false if id is used for something else other than primary key.
|
119
|
+
self.id_is_primary_key_param = true
|
120
|
+
|
121
|
+
# Used when paging is enabled.
|
122
|
+
self.number_of_records_in_a_page = 15
|
123
|
+
|
124
|
+
# Included if the action method exists when `extensions` is called.
|
125
|
+
self.autoincludes = {
|
126
|
+
create: [:smart_layout, :query_includes],
|
127
|
+
destroy: [:smart_layout, :query_includes],
|
128
|
+
edit: [:smart_layout, :query_includes],
|
129
|
+
index: [:smart_layout, :index_query, :order, :param_filters, :params_to_joins, :query_filter, :query_includes],
|
130
|
+
new: [:smart_layout],
|
131
|
+
show: [:smart_layout, :query_includes],
|
132
|
+
update: [:smart_layout, :query_includes]
|
133
|
+
}
|
134
|
+
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
You may want to put your configuration in an initializer like `config/initializers/irie.rb`.
|
139
|
+
|
140
|
+
### Controller Configuration
|
141
|
+
|
142
|
+
The default controller config may be fine, but you can customize it.
|
143
|
+
|
144
|
+
In the controller, you can set a variety of class attributes with `self.something = ...` in the body of your controller.
|
145
|
+
|
146
|
+
All of the app-level configuration parameters are configurable in the controller class body, e.g.:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
self.can_filter_by_default_using = [:eq]
|
150
|
+
self.function_param_names = {}
|
151
|
+
self.predicate_prefix = '.'
|
152
|
+
self.number_of_records_in_a_page = 15
|
153
|
+
self.id_is_primary_key_param = true
|
154
|
+
self.update_should_return_entity = false
|
155
|
+
```
|
156
|
+
|
157
|
+
#### About Extensions
|
158
|
+
|
159
|
+
As you may have noticed in `autoincludes`, some concerns are included as a package along with the action include.
|
160
|
+
|
161
|
+
The following assumes that you are using the default autoincludes and included the relevant action.
|
162
|
+
|
163
|
+
#### Filtering by Attribute(s)
|
164
|
+
|
165
|
+
Inherited Resources has `has_scope` via the [has_scope][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.
|
166
|
+
|
167
|
+
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.
|
168
|
+
|
169
|
+
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:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
can_filter_by :title
|
173
|
+
```
|
174
|
+
|
175
|
+
allows you to:
|
176
|
+
|
177
|
+
```
|
178
|
+
http://localhost:3000/posts?title=Awesome
|
179
|
+
```
|
180
|
+
|
181
|
+
or
|
182
|
+
|
183
|
+
```
|
184
|
+
http://localhost:3000/posts?title.eq=Awesome
|
185
|
+
```
|
186
|
+
|
187
|
+
Since `.eq` is optional in the param name.
|
188
|
+
|
189
|
+
And, like `has_scope`, predications are supported. Do `Arel::Predications.public_instance_methods.sort` in Rails console to see the list:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
:does_not_match, :does_not_match_all, :does_not_match_any, :eq, :eq_all, :eq_any, :gt,
|
193
|
+
:gt_all, :gt_any, :gteq, :gteq_all, :gteq_any, :in, :in_all, :in_any, :lt, :lt_all,
|
194
|
+
:lt_any, :lteq, :lteq_all, :lteq_any, :matches, :matches_all, :matches_any, :not_eq,
|
195
|
+
:not_eq_all, :not_eq_any, :not_in, :not_in_all, :not_in_any
|
196
|
+
```
|
197
|
+
|
198
|
+
You can specify these via the `using:` option:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
can_filter_by :seen_on, using: [:gteq, :eq_any]
|
202
|
+
```
|
203
|
+
|
204
|
+
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:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
can_filter_by :mileage, using: [:eq_any], split: ","
|
208
|
+
```
|
209
|
+
|
210
|
+
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][arel] predicate you allowed, e.g.:
|
211
|
+
|
212
|
+
```
|
213
|
+
http://localhost:3000/posts?seen_on.gteq=2012-08-08
|
214
|
+
```
|
215
|
+
|
216
|
+
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.:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
can_filter_by :name, through: {company: {employee: :full_name}}
|
220
|
+
```
|
221
|
+
|
222
|
+
If a MagicalUnicorn `has_many :friends` and a MagicalUnicorn's friend has a name attribute:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
can_filter_by :magical_unicorn_friend_name,
|
226
|
+
through: {magical_unicorns:{friends: :name}}
|
227
|
+
```
|
228
|
+
|
229
|
+
and use this to get valleys associated with unicorns who in turn have a friend named Oscar:
|
230
|
+
|
231
|
+
```
|
232
|
+
http://localhost:3000/magical_valleys?magical_unicorn_friend_name=Oscar
|
233
|
+
```
|
234
|
+
|
235
|
+
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.:
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
can_filter_by_query a_request_param_name: ->(q, param_value_or_values) {
|
239
|
+
q.joins(:some_assoc).where(some_assocs_table_name: {some_attr: param_value_or_values})
|
240
|
+
}
|
241
|
+
```
|
242
|
+
|
243
|
+
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.
|
244
|
+
|
245
|
+
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.
|
246
|
+
|
247
|
+
##### Customizing Request Parameter Value Conversion
|
248
|
+
|
249
|
+
Implement the `convert_param(param_name, param_values)` in your controller or an included module, e.g.
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
# example of custom parameter value converter
|
253
|
+
module Example
|
254
|
+
module BooleanParams
|
255
|
+
extend ::ActiveSupport::Concern
|
256
|
+
|
257
|
+
TRUE_VALUE = 'true'.freeze
|
258
|
+
FALSE_VALUE = 'false'.freeze
|
259
|
+
|
260
|
+
protected
|
261
|
+
|
262
|
+
# Converts request param value(s) 'true' to true and 'false' to false
|
263
|
+
def convert_param(param_name, param_value_or_values)
|
264
|
+
logger.debug("Example::BooleanParams.convert_param(#{param_name.inspect}, #{param_value_or_values.inspect})") if ::Irie.debug?
|
265
|
+
param_value_or_values = super if defined?(super)
|
266
|
+
if param_value_or_values.is_a? Array
|
267
|
+
param_value_or_values.map {|v| convert_boolean(v)}
|
268
|
+
else
|
269
|
+
convert_boolean(param_value_or_values)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
private
|
274
|
+
|
275
|
+
def convert_boolean(value)
|
276
|
+
case value
|
277
|
+
when TRUE_VALUE
|
278
|
+
true
|
279
|
+
when FALSE_VALUE
|
280
|
+
false
|
281
|
+
else
|
282
|
+
value
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
#### Default Filters
|
290
|
+
|
291
|
+
Like the combination of `scope` and `has_scope` available via [has_scope][has_scope], which you can still use, defaults are supported that are compatible with `can_filter_by`.
|
292
|
+
|
293
|
+
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:
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
can_filter_by :attr_name_1
|
297
|
+
can_filter_by :production_date, :creation_date, using: [:gt, :eq, :lteq]
|
298
|
+
default_filter_by :attr_name_1, eq: 5
|
299
|
+
default_filter_by :production_date, :creation_date, gt: 1.year.ago,
|
300
|
+
lteq: 1.year.from_now
|
301
|
+
```
|
302
|
+
|
303
|
+
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.
|
304
|
+
|
305
|
+
#### Extensions
|
306
|
+
|
307
|
+
##### Count
|
308
|
+
|
309
|
+
In the controller:
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
extensions :count
|
313
|
+
```
|
314
|
+
|
315
|
+
enables:
|
316
|
+
|
317
|
+
```
|
318
|
+
http://localhost:3000/posts?count=
|
319
|
+
```
|
320
|
+
|
321
|
+
That will set the `@count` instance variable that you can use in your view.
|
322
|
+
|
323
|
+
Use `extensions :autorender_count` to render count automatically for non-HTML (JSON, etc.) views.
|
324
|
+
|
325
|
+
##### Page Count
|
326
|
+
|
327
|
+
In the controller:
|
328
|
+
|
329
|
+
```ruby
|
330
|
+
extensions :paging
|
331
|
+
```
|
332
|
+
|
333
|
+
enables:
|
334
|
+
|
335
|
+
```
|
336
|
+
http://localhost:3000/posts?page_count=
|
337
|
+
```
|
338
|
+
|
339
|
+
That will set the `@page_count` instance variable that you can use in your view.
|
340
|
+
|
341
|
+
Use `extensions :autorender_page_count` to render count automatically for non-HTML (JSON, etc.) views.
|
342
|
+
|
343
|
+
##### Getting a Page
|
344
|
+
|
345
|
+
In the controller:
|
346
|
+
|
347
|
+
```ruby
|
348
|
+
extensions :paging
|
349
|
+
```
|
350
|
+
|
351
|
+
To access each page of results:
|
352
|
+
|
353
|
+
```
|
354
|
+
http://localhost:3000/posts?page=1
|
355
|
+
http://localhost:3000/posts?page=2
|
356
|
+
```
|
357
|
+
|
358
|
+
To set page size at application level:
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
::Irie.number_of_records_in_a_page = 15
|
362
|
+
```
|
363
|
+
|
364
|
+
To set page size at controller level:
|
365
|
+
|
366
|
+
```ruby
|
367
|
+
self.number_of_records_in_a_page = 15
|
368
|
+
```
|
369
|
+
|
370
|
+
##### Offset and Limit
|
371
|
+
|
372
|
+
In the controller:
|
373
|
+
|
374
|
+
```ruby
|
375
|
+
extensionss :offset, :limit
|
376
|
+
```
|
377
|
+
|
378
|
+
enables:
|
379
|
+
|
380
|
+
```
|
381
|
+
http://localhost:3000/posts?offset=5
|
382
|
+
http://localhost:3000/posts?limit=5
|
383
|
+
```
|
384
|
+
|
385
|
+
You can combine them to act like page:
|
386
|
+
|
387
|
+
```
|
388
|
+
http://localhost:3000/posts?limit=15
|
389
|
+
http://localhost:3000/posts?offset=15&limit=15
|
390
|
+
http://localhost:3000/posts?offset=30&limit=15
|
391
|
+
```
|
392
|
+
|
393
|
+
#### Order
|
394
|
+
|
395
|
+
You can allow request specified order:
|
396
|
+
|
397
|
+
```ruby
|
398
|
+
can_order_by :foo_date, :foo_color
|
399
|
+
```
|
400
|
+
|
401
|
+
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:
|
402
|
+
|
403
|
+
```
|
404
|
+
http://localhost:3000/posts?order=foo_date,-foo_color
|
405
|
+
```
|
406
|
+
|
407
|
+
The `default_order_by` specifies an ordered array of ascending attributes and/or hashes of attributes to sort direction:
|
408
|
+
|
409
|
+
```ruby
|
410
|
+
default_order_by :posted_at => :desc, :id => :desc
|
411
|
+
```
|
412
|
+
|
413
|
+
or:
|
414
|
+
|
415
|
+
```ruby
|
416
|
+
default_order_by {:this_is_desc => :desc}, :this_is_asc,
|
417
|
+
{:no_different_than_a_symbol => :asc},
|
418
|
+
:this_is_asc_also, :id => :desc
|
419
|
+
```
|
420
|
+
|
421
|
+
`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`.
|
422
|
+
|
423
|
+
#### Custom Index Queries
|
424
|
+
|
425
|
+
To filter the list where the status_code attribute is 'green':
|
426
|
+
|
427
|
+
```ruby
|
428
|
+
index_query ->(q) { q.where(:status_code => 'green') }
|
429
|
+
```
|
430
|
+
|
431
|
+
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][arel]/[ActiveRecord relations][ar]), e.g. to filter the list where the object's apples and pears associations are green:
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
index_query ->(q) {
|
435
|
+
q.joins(:apples, :pears)
|
436
|
+
.where(apples: {color: 'green'})
|
437
|
+
.where(pears: {color: 'green'})
|
438
|
+
}
|
439
|
+
```
|
440
|
+
|
441
|
+
To avoid n+1 queries, use `.includes(...)` in your query to eager load any associations that you will need in the JSON view.
|
442
|
+
|
443
|
+
#### Smart Layout
|
444
|
+
|
445
|
+
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.
|
446
|
+
|
447
|
+
#### Avoid n+1 Queries
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
# load all the posts and the associated category and comments for each post
|
451
|
+
query_includes :category, :comments
|
452
|
+
```
|
453
|
+
|
454
|
+
or
|
455
|
+
|
456
|
+
```ruby
|
457
|
+
# load all of the associated posts, the associated posts’ tags and comments, and every comment’s guest association
|
458
|
+
query_includes posts: [{comments: :guest}, :tags]
|
459
|
+
```
|
460
|
+
|
461
|
+
and action-specific:
|
462
|
+
|
463
|
+
```ruby
|
464
|
+
query_includes_for :create, are: [:category, :comments]
|
465
|
+
query_includes_for :index, :show, are: [posts: [{comments: :guest}, :tags]]
|
466
|
+
```
|
467
|
+
|
468
|
+
#### Using define_params vs :through option
|
469
|
+
|
470
|
+
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.
|
471
|
+
|
472
|
+
```ruby
|
473
|
+
define_params name: {company: {employee: :full_name}},
|
474
|
+
color: :external_color
|
475
|
+
can_filter_by :name
|
476
|
+
default_filter_by :name, eq: 'Guest'
|
477
|
+
can_order_by :color
|
478
|
+
default_filter_by :color, eq: 'blue'
|
479
|
+
```
|
480
|
+
|
481
|
+
#### Other Extensions
|
482
|
+
|
483
|
+
The following concerns, which you can include via `extensions ...` or via including the corresponding module, might also be of use in your controller:
|
484
|
+
|
485
|
+
* `:nil_params` - convert 'NULL', 'null', and 'nil' to nil when passed in as request params.
|
486
|
+
|
487
|
+
#### Writing Your Own Extensions
|
488
|
+
|
489
|
+
Extensions are just modules. There is no magic.
|
490
|
+
|
491
|
+
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.
|
492
|
+
|
493
|
+
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.
|
494
|
+
|
495
|
+
Here's another example:
|
496
|
+
|
497
|
+
```ruby
|
498
|
+
# Converts all 'true' and 'false' param values to true and false
|
499
|
+
module BooleanParams
|
500
|
+
extend ::ActiveSupport::Concern
|
501
|
+
|
502
|
+
protected
|
503
|
+
|
504
|
+
def convert_param(param_name, param_values)
|
505
|
+
case param_values
|
506
|
+
when 'true'
|
507
|
+
true
|
508
|
+
when 'false'
|
509
|
+
false
|
510
|
+
else
|
511
|
+
super if defined?(super)
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
end
|
516
|
+
```
|
517
|
+
|
518
|
+
If you are just doing regular `include`'s in your controllers, that's all you need. If you'd like to use `extensions`, you get autoincludes and can use symbols, e.g. in `app/controllers/concerns/service_controller.rb`:
|
519
|
+
|
520
|
+
```ruby
|
521
|
+
module ServiceController
|
522
|
+
extend ::ActiveSupport::Concern
|
523
|
+
|
524
|
+
included do
|
525
|
+
inherit_resources
|
526
|
+
respond_to :json
|
527
|
+
|
528
|
+
# reference as string so we don't load the concern before it is used.
|
529
|
+
::Irie.register_extension :boolean_params, '::Example::BooleanParams'
|
530
|
+
|
531
|
+
# register_extension also can define the order of inclusion, e.g.:
|
532
|
+
# ::Irie.register_extension :boolean_params, '::Example::BooleanParams', include: :last #default
|
533
|
+
# ::Irie.register_extension :boolean_params, '::Example::BooleanParams', include: :first
|
534
|
+
# ::Irie.register_extension :boolean_params, '::Example::BooleanParams', after: :nil_params
|
535
|
+
# ::Irie.register_extension :boolean_params, '::Example::BooleanParams', before: :nil_params
|
536
|
+
|
537
|
+
end
|
538
|
+
|
539
|
+
end
|
540
|
+
```
|
541
|
+
|
542
|
+
Now you could use this in your controller:
|
543
|
+
|
544
|
+
```ruby
|
545
|
+
include ServiceController
|
546
|
+
|
547
|
+
actions :index
|
548
|
+
extensions :boolean_params
|
549
|
+
```
|
550
|
+
|
551
|
+
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.
|
552
|
+
|
553
|
+
#### Primary Keys
|
554
|
+
|
555
|
+
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.
|
556
|
+
|
557
|
+
#### Exception Handling
|
558
|
+
|
559
|
+
Rails 4 has basic exception handling in the [public_exceptions][public_exceptions] and [show_exceptions][show_exceptions] Rack middleware.
|
560
|
+
|
561
|
+
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.
|
562
|
+
|
563
|
+
You can also use `rescue_from` or `around_action` in Rails to have more control over error rendering.
|
564
|
+
|
565
|
+
### Troubleshooting
|
566
|
+
|
567
|
+
#### Irie::Extensions::QueryIncludes
|
568
|
+
|
569
|
+
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.
|
570
|
+
|
571
|
+
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):
|
572
|
+
|
573
|
+
```ruby
|
574
|
+
index_query ->(q) {
|
575
|
+
# Using standard joins performs an INNER JOIN like we want, but doesn't
|
576
|
+
# eager load.
|
577
|
+
# Using includes does an eager load, but does a LEFT OUTER JOIN, which
|
578
|
+
# isn't really what we want, but in this scenario is probably ok.
|
579
|
+
# Using standard joins & includes results in bad SQL with table aliases.
|
580
|
+
# So, using includes & custom joins seems like a decent solution.
|
581
|
+
q.includes(:bartender, :waitress, :owner, :customer)
|
582
|
+
.joins('INNER JOIN employees bartenders ON bartenders.employee_id = ' +
|
583
|
+
'shifts.bartender_id')
|
584
|
+
.joins('INNER JOIN waitresses shift_workers ON shift_workers.id = ' +
|
585
|
+
'shifts.waitress_id')
|
586
|
+
.where(bartenders: {certified: 'yes'})
|
587
|
+
.where(shift_workers: {attitude: 'great'})
|
588
|
+
}
|
589
|
+
|
590
|
+
# set includes for all actions except index
|
591
|
+
query_includes :owner, :customer, :bartender, :waitress
|
592
|
+
|
593
|
+
# includes specified in index query
|
594
|
+
query_includes_for :index, are: []
|
595
|
+
```
|
596
|
+
|
597
|
+
#### Debugging Includes
|
598
|
+
|
599
|
+
##### Logging
|
600
|
+
|
601
|
+
If you enabled Irie's debug option via:
|
602
|
+
|
603
|
+
```ruby
|
604
|
+
::Irie.debug = true
|
605
|
+
```
|
606
|
+
|
607
|
+
Then all the included modules (actions, extensions) will use `logger.debug ...` to log some information about what is executed.
|
608
|
+
|
609
|
+
To log debug to console only in your tests, you could put this in your test helper:
|
610
|
+
|
611
|
+
```ruby
|
612
|
+
::Irie.debug = true
|
613
|
+
::ActionController::Base.logger = Logger.new(STDOUT)
|
614
|
+
::ActionController::Base.logger.level = Logger::DEBUG
|
615
|
+
```
|
616
|
+
|
617
|
+
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`:
|
618
|
+
|
619
|
+
```ruby
|
620
|
+
::Irie.debug = true
|
621
|
+
config.log_level = :debug
|
622
|
+
```
|
623
|
+
|
624
|
+
### restful_json
|
625
|
+
|
626
|
+
The project was originally named [restful_json][restful_json]. Old commit tags corresponding to restful_json versions may be found in [legacy][legacy].
|
627
|
+
|
628
|
+
### Release Notes
|
629
|
+
|
630
|
+
See [changelog][changelog] and git log.
|
631
|
+
|
632
|
+
### Contributing
|
633
|
+
|
634
|
+
Please fork, make changes in a separate branch, and do a pull request. Thanks!
|
635
|
+
|
636
|
+
### Authors
|
637
|
+
|
638
|
+
This was written by [FineLine Prototyping, Inc.](http://www.finelineprototyping.com) by the following contributors:
|
639
|
+
* [Gary Weaver](https://github.com/garysweaver)
|
640
|
+
* [Tommy Odom](https://github.com/tpodom)
|
641
|
+
|
642
|
+
### License
|
643
|
+
|
644
|
+
Copyright (c) 2013 FineLine Prototyping, Inc., released under the [MIT license][lic].
|
645
|
+
|
646
|
+
[travis]: http://travis-ci.org/FineLinePrototyping/irie
|
647
|
+
[badgefury]: http://badge.fury.io/rb/irie
|
648
|
+
[arel]: https://github.com/rails/arel
|
649
|
+
[ar]: http://api.rubyonrails.org/classes/ActiveRecord/Relation.html
|
650
|
+
[has_scope]: https://github.com/plataformatec/has_scope
|
651
|
+
[public_exceptions]: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
|
652
|
+
[show_exceptions]: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
|
653
|
+
[changelog]: https://github.com/FineLinePrototyping/irie/blob/master/CHANGELOG.md
|
654
|
+
[inherited_resources]: https://github.com/josevalim/inherited_resources
|
655
|
+
[restful_json]: http://rubygems.org/gems/restful_json
|
656
|
+
[legacy]: http://github.com/FineLinePrototyping/irie/blob/master/LEGACY.md
|
657
|
+
[lic]: http://github.com/FineLinePrototyping/irie/blob/master/LICENSE
|