rice_cooker 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/Gemfile.lock +4 -4
- data/README.md +27 -2
- data/lib/rice_cooker/base/base.rb +19 -0
- data/lib/rice_cooker/{helpers.rb → base/helpers.rb} +111 -0
- data/lib/rice_cooker/filter.rb +78 -41
- data/lib/rice_cooker/search.rb +51 -0
- data/lib/rice_cooker/version.rb +1 -1
- data/lib/rice_cooker.rb +21 -18
- data/spec/mocks/controllers/users_controller.rb +1 -0
- data/spec/search/search_spec.rb +267 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 929e7389b2a4a7d2d42b2c69cd6216886e6ddf3c
|
4
|
+
data.tar.gz: 4c58c53af490da81965005546c814966117b94ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ecd88c5dd48b17eb2a34a4307e3380843c5c74e7781076c0e6f3120c3f192839e9c928b187ac1d4fc2e04bfd623ba37ad3b328efbf605bd97baa0ab73dc75cf
|
7
|
+
data.tar.gz: 21ee114e133548192359a057d939cacdc3189eed43e059e1803796785508a67f7743abdab0213a3f8ededf4acf4ab631c9d494d57e03d1d7df111c8453587d80
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rice_cooker (0.1.
|
4
|
+
rice_cooker (0.1.4)
|
5
5
|
actionpack (>= 4.2.0, < 5.1)
|
6
6
|
has_scope (~> 0.7.0, >= 0.6.0)
|
7
7
|
rails (>= 4.2.0, < 5.1)
|
@@ -47,7 +47,7 @@ GEM
|
|
47
47
|
i18n (~> 0.7)
|
48
48
|
minitest (~> 5.1)
|
49
49
|
tzinfo (~> 1.1)
|
50
|
-
arel (7.
|
50
|
+
arel (7.1.1)
|
51
51
|
ast (2.3.0)
|
52
52
|
builder (3.2.2)
|
53
53
|
coderay (1.1.1)
|
@@ -63,7 +63,7 @@ GEM
|
|
63
63
|
railties (>= 3.0.0)
|
64
64
|
faker (1.6.1)
|
65
65
|
i18n (~> 0.5)
|
66
|
-
globalid (0.3.
|
66
|
+
globalid (0.3.7)
|
67
67
|
activesupport (>= 4.1.0)
|
68
68
|
has_scope (0.7.0)
|
69
69
|
actionpack (>= 4.1, < 5.1)
|
@@ -163,7 +163,7 @@ GEM
|
|
163
163
|
simplecov-html (~> 0.10.0)
|
164
164
|
simplecov-html (0.10.0)
|
165
165
|
slop (3.6.0)
|
166
|
-
sprockets (3.
|
166
|
+
sprockets (3.7.0)
|
167
167
|
concurrent-ruby (~> 1.0)
|
168
168
|
rack (> 1, < 3)
|
169
169
|
sprockets-rails (3.1.1)
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Rice Cooker
|
2
2
|
|
3
|
-
[![Build Status](https://travis-ci.
|
3
|
+
[![Build Status](https://travis-ci.org/lambda2/rice_cooker.svg?branch=master)](https://travis-ci.org/lambda2/rice_cooker)
|
4
4
|
|
5
5
|
Handle sort, filters, searches, and ranges on Rails collections.
|
6
6
|
|
@@ -11,6 +11,7 @@ Handle sort, filters, searches, and ranges on Rails collections.
|
|
11
11
|
In your `Gemfile`:
|
12
12
|
|
13
13
|
```ruby
|
14
|
+
gem 'has_scope'
|
14
15
|
gem 'rice_cooker'
|
15
16
|
```
|
16
17
|
|
@@ -35,4 +36,28 @@ class UsersController < ActionController::Base
|
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
38
|
-
```
|
39
|
+
```
|
40
|
+
|
41
|
+
### Sorting
|
42
|
+
|
43
|
+
The `sort` parameter allow sorting on one or several, comma-separated, fields. The sort is applied in the order specified.
|
44
|
+
The sort order for each sort field is ascending unless it is prefixed with a minus (-), in which case it is descending.
|
45
|
+
|
46
|
+
For example, the `api.example.org/unicorns?sort=color,-name` url will return all the unicorns order by **color** in the ascending order, and if they have the same color, by **name**, on the descending order.
|
47
|
+
|
48
|
+
### Filtering
|
49
|
+
|
50
|
+
The `filter` parameter is a hash allowing filtering for a field, as a key, on one or several, comma-separated, values. Only the fields matching the given filter(s) will be returned.
|
51
|
+
|
52
|
+
For example, the `api.example.org/unicorns?filter[color]=yellow&filter[age]=21,22` url will return all the **yellow** unicorns which are **21** OR **22** years old.
|
53
|
+
|
54
|
+
### Ranging
|
55
|
+
|
56
|
+
The `range` parameter is a hash allowing filtering for a field, as a key, on two comma-separated bounds. Only the fields between the given bounds will be returned. The bounds are inclusives.
|
57
|
+
|
58
|
+
For example, the `api.example.org/unicorns?range[age]=21,42` url will return all the unicorns which are between **21** and **42** years old.
|
59
|
+
|
60
|
+
|
61
|
+
## Specification
|
62
|
+
|
63
|
+
The gem try to follow the [RAPIS](https://github.com/lambda2/rapis) api convention.
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module RiceCooker
|
3
|
+
class Base
|
4
|
+
include Helpers
|
5
|
+
attr_accessor :params
|
6
|
+
attr_accessor :model
|
7
|
+
attr_accessor :allowed_keys
|
8
|
+
|
9
|
+
def initialize(unformated_params, model)
|
10
|
+
@params = format_additional_param((unformated_params || {}), self.class.action)
|
11
|
+
@model = model
|
12
|
+
@allowed_keys = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.action
|
16
|
+
raise "You must override action"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -11,6 +11,12 @@ module RiceCooker
|
|
11
11
|
class InvalidFilterValueException < Exception
|
12
12
|
end
|
13
13
|
|
14
|
+
class InvalidSearchException < Exception
|
15
|
+
end
|
16
|
+
|
17
|
+
class InvalidSearchValueException < Exception
|
18
|
+
end
|
19
|
+
|
14
20
|
class InvalidRangeException < Exception
|
15
21
|
end
|
16
22
|
|
@@ -42,6 +48,15 @@ module RiceCooker
|
|
42
48
|
end
|
43
49
|
end
|
44
50
|
|
51
|
+
# Overridable method for available rangeable fields
|
52
|
+
def searchable_fields_for(model)
|
53
|
+
if model.respond_to?(:searchable_fields)
|
54
|
+
model.rangeable_fields.map(&:to_sym)
|
55
|
+
else
|
56
|
+
filterable_fields_for(model)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
45
60
|
# Overridable method for available rangeable fields
|
46
61
|
def rangeable_fields_for(model)
|
47
62
|
if model.respond_to?(:rangeable_fields)
|
@@ -180,6 +195,102 @@ module RiceCooker
|
|
180
195
|
collection
|
181
196
|
end
|
182
197
|
|
198
|
+
|
199
|
+
# ------------------------ Search helpers --------------------
|
200
|
+
|
201
|
+
# Va transformer le param url en hash exploitable
|
202
|
+
def parse_searching_param(searching_param, allowed_params)
|
203
|
+
return {} unless searching_param.present?
|
204
|
+
|
205
|
+
fields = {}
|
206
|
+
|
207
|
+
# Extract the fields for each type from the fields parameters
|
208
|
+
if searching_param.is_a?(Hash)
|
209
|
+
searching_param.each do |field, value|
|
210
|
+
resource_fields = value.split(',') unless value.nil? || value.empty?
|
211
|
+
fields[field.to_sym] = resource_fields
|
212
|
+
end
|
213
|
+
else
|
214
|
+
raise InvalidSearchException, "Invalid search format for #{searching_param}"
|
215
|
+
end
|
216
|
+
check_searching_param(fields, allowed_params)
|
217
|
+
fields
|
218
|
+
end
|
219
|
+
|
220
|
+
# Our little barrier <3
|
221
|
+
def check_searching_param(searching_param, allowed)
|
222
|
+
🔞 = searching_param.keys.map(&:to_sym) - allowed.map(&:to_sym)
|
223
|
+
raise InvalidSearchException, "Attributes #{🔞.map(&:to_s).to_sentence} doesn't exists or aren't searchables. Available searchs are: #{allowed.to_sentence}" if 🔞.any?
|
224
|
+
end
|
225
|
+
|
226
|
+
# On va essayer de garder un format commun, qui est:
|
227
|
+
#
|
228
|
+
# ```
|
229
|
+
# search: {
|
230
|
+
# proc: -> (values) { * je fais des trucs avec les values * },
|
231
|
+
# all: ['les', 'valeurs', 'aceptées'],
|
232
|
+
# description: "La description dans la doc"
|
233
|
+
# }
|
234
|
+
# ```
|
235
|
+
#
|
236
|
+
# On va donc transformer `additional` dans le format ci-dessus
|
237
|
+
#
|
238
|
+
def format_additional_param(additional, context_format = 'searching')
|
239
|
+
if additional.is_a? Hash
|
240
|
+
additional = additional.map do |field, value|
|
241
|
+
if value.is_a?(Hash)
|
242
|
+
value = {
|
243
|
+
proc: nil,
|
244
|
+
all: [],
|
245
|
+
description: ''
|
246
|
+
}.merge(value)
|
247
|
+
elsif value.is_a? Array
|
248
|
+
value = {
|
249
|
+
proc: value.try(:at, 0),
|
250
|
+
all: value.try(:at, 1) || [],
|
251
|
+
description: value.try(:at, 2) || ''
|
252
|
+
}
|
253
|
+
elsif value.is_a? Proc
|
254
|
+
value = {
|
255
|
+
proc: value,
|
256
|
+
all: [],
|
257
|
+
description: ''
|
258
|
+
}
|
259
|
+
else
|
260
|
+
raise "Unable to format addional #{context_format} params (got #{additional})"
|
261
|
+
end
|
262
|
+
[field, value]
|
263
|
+
end.to_h
|
264
|
+
end
|
265
|
+
additional
|
266
|
+
end
|
267
|
+
|
268
|
+
def reduce_where(col, field, value)
|
269
|
+
reducer = nil
|
270
|
+
value.each do |v|
|
271
|
+
query = col.model.arel_table[field.to_sym].matches("%#{v.to_s}%")
|
272
|
+
reducer = (reducer ? reducer.or(query) : query)
|
273
|
+
end
|
274
|
+
col.where(reducer)
|
275
|
+
end
|
276
|
+
|
277
|
+
def apply_search_to_collection(col, searching_params, additional = {})
|
278
|
+
return col if col.nil?
|
279
|
+
|
280
|
+
searching_params.each do |field, value|
|
281
|
+
if additional.key?(field) && additional[field].key?(:proc)
|
282
|
+
col = col.instance_exec(value, &(additional[field][:proc]))
|
283
|
+
elsif value.is_a?(String)
|
284
|
+
col = col.where(col.model.arel_table[field.to_sym].matches("%#{value.join(' ')}%"))
|
285
|
+
elsif value.is_a?(Array)
|
286
|
+
col = reduce_where(col, field, value)
|
287
|
+
elsif value.is_a?(Hash) && value.key?(:proc)
|
288
|
+
col
|
289
|
+
end
|
290
|
+
end
|
291
|
+
col
|
292
|
+
end
|
293
|
+
|
183
294
|
# ------------------------ Range helpers --------------------
|
184
295
|
|
185
296
|
# Va transformer le param url en hash exploitable
|
data/lib/rice_cooker/filter.rb
CHANGED
@@ -6,52 +6,84 @@ module RiceCooker
|
|
6
6
|
|
7
7
|
FILTER_PARAM = :filter
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
class Filter < RiceCooker::Base
|
10
|
+
|
11
|
+
def self.action
|
12
|
+
:filtering
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(unformated_params, model)
|
16
|
+
super
|
17
|
+
@allowed_keys = (filterable_fields_for(@model) + @params.keys)
|
18
|
+
end
|
19
|
+
|
20
|
+
def register_bools
|
21
|
+
additional = (filterable_fields_for(@model) - [:created_at, :updated_at])
|
22
|
+
.select { |e| e =~ /_at$/ }
|
23
|
+
.select { |e| @params[e.to_s.gsub(/_at$/, '')].nil? }
|
24
|
+
additional.each { |fi| parse_bool(fi) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse_bool(fi)
|
28
|
+
if fi.to_sym == :begin_at
|
29
|
+
parse_future_bool fi
|
30
|
+
else
|
31
|
+
parse_named_bool fi
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_future_bool(fi)
|
36
|
+
@params[:future] = {
|
37
|
+
proc: Filer.get_future_lambda("#{@model.quoted_table_name}.\"#{fi}\""),
|
38
|
+
all: %w(true false),
|
39
|
+
description: "Return only #{@model.to_s.underscore.humanize.downcase.pluralize} which begins in the future"
|
40
|
+
}
|
41
|
+
@allowed_keys << :future
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_named_bool(fi)
|
45
|
+
name = fi.to_s.gsub(/_at$/, '')
|
46
|
+
@params[name.to_sym] = {
|
47
|
+
proc: Filer.get_named_lambda(fi),
|
48
|
+
all: %w(true false),
|
49
|
+
description: "Return only #{name} #{@model.to_s.underscore.humanize.downcase.pluralize}"
|
50
|
+
}
|
51
|
+
@allowed_keys << name
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.get_future_lambda(db_field)
|
55
|
+
lambda do |value|
|
56
|
+
value.first == 'true' ? where("#{db_field} >= ?", Time.zone.now) : where("#{db_field} < ?", Time.zone.now)
|
57
|
+
end
|
58
|
+
end
|
11
59
|
|
60
|
+
def self.get_named_lambda(fi)
|
61
|
+
lambda do |value|
|
62
|
+
value.first == 'true' ? where.not(fi => nil) : where(fi => nil)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def process(value, scope, custom, filter)
|
67
|
+
params = parse_filtering_param(value, filter)
|
68
|
+
apply_filter_to_collection(scope, params, custom)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module ClassMethods
|
12
73
|
def filtered(additional_filtering_params = {})
|
13
74
|
cattr_accessor :filtering_keys
|
14
75
|
cattr_accessor :custom_filters
|
15
76
|
|
16
|
-
|
17
|
-
additional_filtering_params = format_additional_param(additional_filtering_params, 'filtering')
|
18
|
-
|
19
|
-
# On recupere tous les filtres autorisés
|
20
|
-
allowed_keys = (filterable_fields_for(resource_model) + additional_filtering_params.keys)
|
77
|
+
filter = Filter.new(additional_filtering_params, resource_model)
|
21
78
|
|
22
|
-
|
23
|
-
additional = (filterable_fields_for(resource_model) - [:created_at, :updated_at])
|
24
|
-
.select { |e| e =~ /_at$/ }
|
25
|
-
.select { |e| additional_filtering_params[e.to_s.gsub(/_at$/, '')].nil? }
|
26
|
-
|
27
|
-
additional.each do |fi|
|
28
|
-
name = fi.to_s.gsub(/_at$/, '')
|
29
|
-
|
30
|
-
if fi.to_sym == :begin_at
|
31
|
-
db_field = "#{resource_model.quoted_table_name}.\"#{fi}\""
|
32
|
-
additional_filtering_params[:future] = {
|
33
|
-
proc: -> (value) { value.first == 'true' ? where("#{db_field} >= ?", Time.zone.now) : where("#{db_field} < ?", Time.zone.now) },
|
34
|
-
all: %w(true false),
|
35
|
-
description: "Return only #{resource_model.to_s.underscore.humanize.downcase.pluralize} which begins in the future"
|
36
|
-
}
|
37
|
-
allowed_keys << :future
|
38
|
-
else
|
39
|
-
additional_filtering_params[name.to_sym] = {
|
40
|
-
proc: -> (value) { value.first == 'true' ? where.not(fi => nil) : where(fi => nil) },
|
41
|
-
all: %w(true false),
|
42
|
-
description: "Return only #{name} #{resource_model.to_s.underscore.humanize.downcase.pluralize}"
|
43
|
-
}
|
44
|
-
allowed_keys << name
|
45
|
-
end
|
46
|
-
end
|
79
|
+
filter.register_bools
|
47
80
|
|
48
81
|
# On recupere le default
|
49
|
-
self.filtering_keys = allowed_keys
|
50
|
-
self.custom_filters =
|
82
|
+
self.filtering_keys = filter.allowed_keys
|
83
|
+
self.custom_filters = filter.params
|
51
84
|
|
52
|
-
has_scope
|
53
|
-
|
54
|
-
scope = apply_filter_to_collection(scope, params, custom_filters)
|
85
|
+
has_scope FILTER_PARAM, type: :hash, only: [:index] do |_controller, scope, value|
|
86
|
+
scope = filter.process(value, scope, custom_filters, filtering_keys)
|
55
87
|
scope
|
56
88
|
end
|
57
89
|
end
|
@@ -61,11 +93,11 @@ module RiceCooker
|
|
61
93
|
#
|
62
94
|
# name: le nom du filtre custom (ex: with_mark)
|
63
95
|
# proc: le filtre, prend un arg `val` qui est un tableau des args du filtre
|
64
|
-
# all: l'ensemble des valeurs acceptées pour le filtre. Laisser nil ou
|
96
|
+
# all: l'ensemble des valeurs acceptées pour le filtre. Laisser nil ou pour tout accepter
|
65
97
|
# description: La description dans la doc
|
66
98
|
#
|
67
99
|
def register_filter(name, proc, all = nil, description = nil)
|
68
|
-
raise "A '#{name}' filter already exists for class #{self.class}" unless
|
100
|
+
raise "A '#{name}' filter already exists for class #{self.class}" unless filter_exists?(name)
|
69
101
|
custom_filters[name] = {
|
70
102
|
proc: proc,
|
71
103
|
all: all || [],
|
@@ -82,14 +114,19 @@ module RiceCooker
|
|
82
114
|
# description: La description dans la doc
|
83
115
|
#
|
84
116
|
def register_bool_filter(name, field, description = nil)
|
85
|
-
raise "A '#{name}' filter already exists for class #{self.class}" unless
|
117
|
+
raise "A '#{name}' filter already exists for class #{self.class}" unless filter_exists?(name)
|
86
118
|
custom_filters[name] = {
|
87
|
-
proc:
|
119
|
+
proc: Filter.get_named_lambda(field),
|
88
120
|
all: %w(true false),
|
89
121
|
description: description || "Return only #{resource_model.to_s.underscore.humanize.downcase.pluralize} with a #{field}"
|
90
122
|
}
|
91
123
|
filtering_keys << name
|
92
124
|
end
|
125
|
+
|
126
|
+
# Check if the given custom filter name already exists
|
127
|
+
def filter_exists?(name)
|
128
|
+
!custom_filters[name].nil?
|
129
|
+
end
|
93
130
|
end
|
94
131
|
end
|
95
132
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module RiceCooker
|
4
|
+
module Search
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
FILTER_PARAM = :search
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
include Helpers
|
11
|
+
|
12
|
+
def searched(additional_searching_params = {})
|
13
|
+
cattr_accessor :searching_keys
|
14
|
+
cattr_accessor :custom_searchs
|
15
|
+
|
16
|
+
# On normalize tout ca
|
17
|
+
additional_searching_params = format_additional_param(additional_searching_params, 'filtering')
|
18
|
+
|
19
|
+
# On recupere tous les filtres autorisés
|
20
|
+
allowed_keys = (searchable_fields_for(resource_model) + additional_searching_params.keys)
|
21
|
+
|
22
|
+
# On recupere le default
|
23
|
+
self.searching_keys = allowed_keys
|
24
|
+
self.custom_searchs = additional_searching_params
|
25
|
+
|
26
|
+
has_scope :search, type: :hash, only: [:index] do |_controller, scope, value|
|
27
|
+
params = parse_searching_param(value, searching_keys)
|
28
|
+
scope = apply_search_to_collection(scope, params, custom_searchs)
|
29
|
+
scope
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Ajoute un filtre custom
|
34
|
+
#
|
35
|
+
# name: le nom du filtre custom (ex: with_mark)
|
36
|
+
# proc: le filtre, prend un arg `val` qui est un tableau des args du filtre
|
37
|
+
# all: l'ensemble des valeurs acceptées pour le filtre. Laisser nil ou [] pour tout accepter
|
38
|
+
# description: La description dans la doc
|
39
|
+
#
|
40
|
+
def register_search(name, proc, all = nil, description = nil)
|
41
|
+
raise "A '#{name}' search already exists for class #{self.class}" unless custom_searchs[name].nil?
|
42
|
+
custom_searchs[name] = {
|
43
|
+
proc: proc,
|
44
|
+
all: all || [],
|
45
|
+
description: description || ''
|
46
|
+
}
|
47
|
+
searching_keys << name
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/rice_cooker/version.rb
CHANGED
data/lib/rice_cooker.rb
CHANGED
@@ -1,29 +1,32 @@
|
|
1
1
|
require 'action_controller'
|
2
2
|
|
3
3
|
module RiceCooker
|
4
|
-
autoload :Helpers,
|
4
|
+
autoload :Helpers, 'rice_cooker/base/helpers'
|
5
|
+
autoload :Base, 'rice_cooker/base/base'
|
5
6
|
autoload :ClassMethods, 'rice_cooker/class_methods'
|
6
|
-
autoload :Filter,
|
7
|
-
autoload :Sort,
|
8
|
-
autoload :Range,
|
9
|
-
autoload :
|
10
|
-
|
7
|
+
autoload :Filter, 'rice_cooker/filter'
|
8
|
+
autoload :Sort, 'rice_cooker/sort'
|
9
|
+
autoload :Range, 'rice_cooker/range'
|
10
|
+
autoload :Search, 'rice_cooker/search'
|
11
|
+
autoload :VERSION, 'rice_cooker/version'
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
extend RiceCooker::ClassMethods
|
13
|
+
def self.rice_cooked(base)
|
14
|
+
base.class_eval do
|
15
|
+
include RiceCooker::Sort
|
16
|
+
include RiceCooker::Filter
|
17
|
+
include RiceCooker::Range
|
18
|
+
include RiceCooker::Search
|
19
|
+
extend RiceCooker::ClassMethods
|
20
20
|
|
21
|
-
|
21
|
+
class_attribute :resource_model, instance_writer: false
|
22
22
|
|
23
|
-
|
24
|
-
end
|
23
|
+
protected :resource_model
|
25
24
|
end
|
25
|
+
end
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
+
module ActionController
|
29
|
+
class Base
|
30
|
+
RiceCooker.rice_cooked(self)
|
28
31
|
end
|
29
32
|
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'pry'
|
2
|
+
require 'rice_cooker'
|
3
|
+
require 'active_record'
|
4
|
+
require 'spec_helper'
|
5
|
+
|
6
|
+
RSpec.describe RiceCooker::Search do
|
7
|
+
include RiceCooker::Helpers
|
8
|
+
|
9
|
+
# class User < ActiveRecord::Base; end
|
10
|
+
|
11
|
+
before do
|
12
|
+
@collection_class = User
|
13
|
+
@allowed_params = searchable_fields_for(@collection_class)
|
14
|
+
@collection = @collection_class.all
|
15
|
+
|
16
|
+
@test_search = {
|
17
|
+
with_the_letter: { proc: -> (value) { where('first_name ILIKE ?', value.map { |e| "%#{e}%" }) } },
|
18
|
+
without_the_letter: { proc: -> (value) { where.not('first_name ILIKE ?', value.map { |e| "%#{e}%" }) } }
|
19
|
+
}
|
20
|
+
|
21
|
+
@proc = -> (value) { value }
|
22
|
+
@all = -> (_value) { [1, 2, 3] }
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'Search params must be okay' do
|
26
|
+
it 'Null searching' do
|
27
|
+
# Default null searching
|
28
|
+
searching_params = parse_searching_param('', @allowed_params)
|
29
|
+
expect(searching_params).to be_eql({})
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'Default searching' do
|
33
|
+
params = {
|
34
|
+
login: 'aaubin'
|
35
|
+
}
|
36
|
+
|
37
|
+
searching_params = parse_searching_param(params, @allowed_params)
|
38
|
+
expect(searching_params).to be_eql(login: ['aaubin'])
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'Double searching' do
|
42
|
+
params = {
|
43
|
+
login: 'aaubin,qbollach'
|
44
|
+
}
|
45
|
+
|
46
|
+
searching_params = parse_searching_param(params, @allowed_params)
|
47
|
+
expect(searching_params).to be_eql(login: %w(aaubin qbollach))
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'Multiple searching' do
|
51
|
+
params = {
|
52
|
+
login: 'aaubin,qbollach,andre',
|
53
|
+
id: '74,75,76'
|
54
|
+
}
|
55
|
+
|
56
|
+
searching_params = parse_searching_param(params, @allowed_params)
|
57
|
+
expect(searching_params).to be_eql(login: %w(aaubin qbollach andre),
|
58
|
+
id: %w(74 75 76))
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'invalid args' do
|
62
|
+
# invalid args
|
63
|
+
|
64
|
+
params = {
|
65
|
+
wtf: 'aaubin,qbollach,andre',
|
66
|
+
id: '74,75,76'
|
67
|
+
}
|
68
|
+
|
69
|
+
expect { parse_searching_param(params, @allowed_params) }.to raise_error(RiceCooker::InvalidSearchException)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'Must apply search to given collection' do
|
74
|
+
it 'Default null searching' do
|
75
|
+
searched_collection = apply_search_to_collection(@collection, {})
|
76
|
+
# puts searched_collection.to_sql
|
77
|
+
expect(searched_collection.to_sql).to match(/^((?!WHERE).)*$/)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'Default searching' do
|
81
|
+
searched_collection = apply_search_to_collection(@collection, login: ['aaubin'])
|
82
|
+
# puts searched_collection.to_sql
|
83
|
+
expect(searched_collection.to_sql).to match(/WHERE/)
|
84
|
+
expect(searched_collection.to_sql).to match(/\"login\" LIKE '%aaubin%'/)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'Double searching' do
|
88
|
+
# Desc searching
|
89
|
+
searched_collection = apply_search_to_collection(@collection, login: %w(aaubin qbollach))
|
90
|
+
# puts searched_collection.to_sql
|
91
|
+
expect(searched_collection.to_sql).to match(/WHERE/)
|
92
|
+
expect(searched_collection.to_sql).to match(/\"login\" LIKE '%aaubin%'/)
|
93
|
+
expect(searched_collection.to_sql).to match(/\"login\" LIKE '%qbollach%'/)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'Multiple searching' do
|
97
|
+
# Desc searching
|
98
|
+
searched_collection = apply_search_to_collection(@collection, login: %w(aaubin qbollach andre),
|
99
|
+
id: %w('74' '75' '76'))
|
100
|
+
# puts searched_collection.to_sql
|
101
|
+
expect(searched_collection.to_sql).to match(/\"login\" LIKE '%aaubin%'/)
|
102
|
+
expect(searched_collection.to_sql).to match(/\"login\" LIKE '%qbollach%'/)
|
103
|
+
expect(searched_collection.to_sql).to match(/\) AND \(/)
|
104
|
+
# puts searched_collection.to_sql
|
105
|
+
expect(searched_collection.to_sql).to match(/\"id\" LIKE 0/)
|
106
|
+
expect(searched_collection.to_sql).to match(/\"id\" LIKE 0/)
|
107
|
+
expect(searched_collection.to_sql).to match(/\) AND \(/)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'Must apply custom searchs to given collection' do
|
112
|
+
it 'Default null searching' do
|
113
|
+
searched_collection = apply_search_to_collection(@collection, {}, @test_search)
|
114
|
+
# puts searched_collection.to_sql
|
115
|
+
expect(searched_collection.to_sql).to match(/^((?!WHERE).)*$/)
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'Default searching' do
|
119
|
+
searched_collection = apply_search_to_collection(@collection, { with_the_letter: ['a'] }, @test_search)
|
120
|
+
# puts searched_collection.to_sql
|
121
|
+
expect(searched_collection.to_sql).to match(/WHERE/)
|
122
|
+
expect(searched_collection.to_sql).to match(/ILIKE/)
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'Double searching' do
|
126
|
+
# Desc searching
|
127
|
+
searched_collection = apply_search_to_collection(@collection, { with_the_letter: ['a'], without_the_letter: ['l'] }, @test_search)
|
128
|
+
# puts searched_collection.to_sql
|
129
|
+
expect(searched_collection.to_sql).to match(/WHERE/)
|
130
|
+
expect(searched_collection.to_sql).to match(/first_name ILIKE '%a%'/)
|
131
|
+
expect(searched_collection.to_sql).to match(/NOT \(first_name ILIKE '%l%'\)/)
|
132
|
+
end
|
133
|
+
|
134
|
+
# it 'invalid args' do
|
135
|
+
# # invalid args
|
136
|
+
# expect do
|
137
|
+
# apply_search_to_collection(
|
138
|
+
# @collection,
|
139
|
+
# { sorted: %w(true baguette) },
|
140
|
+
# format_additional_param(sorted: [-> (v) { v }, %w(true false maybe)])
|
141
|
+
# )
|
142
|
+
# end.to raise_error(RiceCooker::InvalidSearchValueException)
|
143
|
+
# end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe 'Additional params must be correctly formated' do
|
147
|
+
it 'No additional params' do
|
148
|
+
formated = format_additional_param({})
|
149
|
+
expect(formated).to be_eql({})
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'Already correctly formatted additional params' do
|
153
|
+
p = { search: {
|
154
|
+
proc: @proc,
|
155
|
+
all: [1, 2, 3],
|
156
|
+
description: 'A good search'
|
157
|
+
} }
|
158
|
+
formated = format_additional_param(p)
|
159
|
+
expect(formated).to be_eql(p)
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'Missing description additional params' do
|
163
|
+
p = { search: {
|
164
|
+
proc: @proc,
|
165
|
+
all: [1, 2, 3]
|
166
|
+
} }
|
167
|
+
expected = { search: {
|
168
|
+
proc: @proc,
|
169
|
+
all: [1, 2, 3],
|
170
|
+
description: ''
|
171
|
+
} }
|
172
|
+
formated = format_additional_param(p)
|
173
|
+
expect(formated).to be_eql(expected)
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'Only proc additional params' do
|
177
|
+
p = { search: @proc }
|
178
|
+
expected = { search: {
|
179
|
+
proc: @proc,
|
180
|
+
all: [],
|
181
|
+
description: ''
|
182
|
+
} }
|
183
|
+
formated = format_additional_param(p)
|
184
|
+
expect(formated).to be_eql(expected)
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'Array with proc and all additional params' do
|
188
|
+
p = { search: [@proc, @all] }
|
189
|
+
expected = { search: {
|
190
|
+
proc: @proc,
|
191
|
+
all: @all,
|
192
|
+
description: ''
|
193
|
+
} }
|
194
|
+
formated = format_additional_param(p)
|
195
|
+
expect(formated).to be_eql(expected)
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'Multiple, std + Array with proc and all additional params' do
|
199
|
+
p = {
|
200
|
+
tata: @proc,
|
201
|
+
toto: { proc: @proc, all: [1, 2] },
|
202
|
+
search: [@proc, @all],
|
203
|
+
tutu: { proc: @proc, description: 'Buuuuh' }
|
204
|
+
}
|
205
|
+
expected = {
|
206
|
+
tata: {
|
207
|
+
proc: @proc,
|
208
|
+
all: [],
|
209
|
+
description: ''
|
210
|
+
},
|
211
|
+
toto: {
|
212
|
+
proc: @proc,
|
213
|
+
all: [1, 2],
|
214
|
+
description: ''
|
215
|
+
},
|
216
|
+
search: {
|
217
|
+
proc: @proc,
|
218
|
+
all: @all,
|
219
|
+
description: ''
|
220
|
+
},
|
221
|
+
tutu: {
|
222
|
+
proc: @proc,
|
223
|
+
all: [],
|
224
|
+
description: 'Buuuuh'
|
225
|
+
}
|
226
|
+
}
|
227
|
+
formated = format_additional_param(p)
|
228
|
+
expect(formated).to be_eql(expected)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
RSpec.describe UsersController, type: :controller do
|
234
|
+
include RiceCooker::Helpers
|
235
|
+
|
236
|
+
before { request.host = 'example.org' }
|
237
|
+
|
238
|
+
describe 'GET #index' do
|
239
|
+
it 'without search parameter' do
|
240
|
+
process :index, method: :get, params: { search: '', format: :json }
|
241
|
+
expect(response.body).to eq(User.all.order(id: :desc).to_json)
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'with simple search parameter' do
|
245
|
+
process :index, method: :get, params: { search: { login: 'a' }, format: :json }
|
246
|
+
expect(response.body).to eq(User.where(User.arel_table[:login].matches("%a%")).order(id: :desc).to_json)
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'with double search parameter' do
|
250
|
+
process :index, method: :get, params: { search: { login: 'a,q' }, format: :json }
|
251
|
+
expect(response.body).to eq(User.where(
|
252
|
+
User.arel_table[:login].matches("%a%").or(User.arel_table[:login].matches("%q%"))
|
253
|
+
).order(id: :desc).to_json)
|
254
|
+
end
|
255
|
+
|
256
|
+
it 'with double and multiple search parameter' do
|
257
|
+
process :index, method: :get, params: { search: { login: 'a,q', email: 't' }, format: :json }
|
258
|
+
expect(response.body).to eq(
|
259
|
+
User.where(
|
260
|
+
User.arel_table[:login].matches("%a%").or(User.arel_table[:login].matches("%q%"))
|
261
|
+
).where(
|
262
|
+
User.arel_table[:email].matches("%t%")
|
263
|
+
).order(id: :desc).to_json
|
264
|
+
)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rice_cooker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andre Aubin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -288,10 +288,12 @@ files:
|
|
288
288
|
- README.md
|
289
289
|
- Rakefile
|
290
290
|
- lib/rice_cooker.rb
|
291
|
+
- lib/rice_cooker/base/base.rb
|
292
|
+
- lib/rice_cooker/base/helpers.rb
|
291
293
|
- lib/rice_cooker/class_methods.rb
|
292
294
|
- lib/rice_cooker/filter.rb
|
293
|
-
- lib/rice_cooker/helpers.rb
|
294
295
|
- lib/rice_cooker/range.rb
|
296
|
+
- lib/rice_cooker/search.rb
|
295
297
|
- lib/rice_cooker/sort.rb
|
296
298
|
- lib/rice_cooker/version.rb
|
297
299
|
- lib/tasks/rice_cooker_tasks.rake
|
@@ -302,6 +304,7 @@ files:
|
|
302
304
|
- spec/mocks/mocks.rb
|
303
305
|
- spec/mocks/models/user.rb
|
304
306
|
- spec/range/range_spec.rb
|
307
|
+
- spec/search/search_spec.rb
|
305
308
|
- spec/sort/sort_spec.rb
|
306
309
|
- spec/spec_helper.rb
|
307
310
|
homepage: https://github.com/lambda2/rice_cooker
|
@@ -335,5 +338,6 @@ test_files:
|
|
335
338
|
- spec/mocks/mocks.rb
|
336
339
|
- spec/mocks/models/user.rb
|
337
340
|
- spec/range/range_spec.rb
|
341
|
+
- spec/search/search_spec.rb
|
338
342
|
- spec/sort/sort_spec.rb
|
339
343
|
- spec/spec_helper.rb
|