lupa 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/.gitignore +24 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +424 -0
- data/Rakefile +11 -0
- data/lib/lupa.rb +11 -0
- data/lib/lupa/scope_methods.rb +13 -0
- data/lib/lupa/search.rb +390 -0
- data/lib/lupa/version.rb +3 -0
- data/lupa.gemspec +24 -0
- data/lupa.png +0 -0
- data/test/array_search_test.rb +131 -0
- data/test/composition_test.rb +41 -0
- data/test/default_scope_search_test.rb +57 -0
- data/test/default_search_attributes_search_test.rb +81 -0
- data/test/test_helper.rb +10 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 83dad7fde61871e8d0333b609958b6eadaccc23c
|
4
|
+
data.tar.gz: 1a1eb1e14ede2f72d5f7eb029c38b2511e9182f6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e6f6127f879df5ec3bcee9e4c0c479df20aa53c17389fedf7a10d1403900da7920372a6bf2dab44ddad82843465c71834fe3343e515f506d63f331b4feec23b3
|
7
|
+
data.tar.gz: 84e3f845e779bdb383410f32509b99b4f7ae5fcda658dbb5e3dd49e8110b735c66b47c49ff583a2942822df800fc656fe8315c2f52bdb3b6807e476a5c7b06e0
|
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
.ruby-gemset
|
24
|
+
.ruby-version
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Ezequiel Delpero
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,424 @@
|
|
1
|
+

|
2
|
+
|
3
|
+
Lupa means *Magnifier* in spanish.
|
4
|
+
|
5
|
+
Lupa lets you create simple, robust and scaleable search filters with ease using regular Ruby classes and object oriented design patterns. It's Framework and ORM agnostic.
|
6
|
+
|
7
|
+
## Search Class
|
8
|
+
|
9
|
+
### Usage
|
10
|
+
|
11
|
+
The example will explain how to use the class on a Rails application:
|
12
|
+
|
13
|
+
- Define a custom form:
|
14
|
+
|
15
|
+
```haml
|
16
|
+
# app/views/products/_search.html.haml
|
17
|
+
|
18
|
+
= form_tag products_path, method: :get do
|
19
|
+
= text_field_tag 'name'
|
20
|
+
= select_tag 'category', options_from_collection_for_select(@categories, 'id', 'name')
|
21
|
+
= date_field_tag 'created_between[start_date]'
|
22
|
+
= date_field_tag 'created_between[end_date]'
|
23
|
+
= submit_tag :search
|
24
|
+
```
|
25
|
+
- Create a new instance of your search class and pass a collection to which all search conditions will be applied and specify the search params you want to apply:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# app/controllers/products_controller.rb
|
29
|
+
|
30
|
+
class ProductsController < ApplicationController
|
31
|
+
def index
|
32
|
+
@products = ProductSearch.new(current_user.products).search(search_params)
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
def search_params
|
37
|
+
params.permit(:name, :category, created_between: [:start_date, :end_date])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
```
|
41
|
+
- Loop through the search results on your view.
|
42
|
+
|
43
|
+
```haml
|
44
|
+
# app/views/products/index.html.haml
|
45
|
+
|
46
|
+
%h1 Products
|
47
|
+
|
48
|
+
%ul
|
49
|
+
- @products.each do |product|
|
50
|
+
%li
|
51
|
+
= "#{product.name} - #{product.price} - #{product.category}"
|
52
|
+
```
|
53
|
+
|
54
|
+
### Definition
|
55
|
+
To define a search class, your class must inherit from **Lupa::Search** and you must define a **Scope** class inside your search class.
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
# app/searches/product_search.rb
|
59
|
+
|
60
|
+
class ProductSearch < Lupa::Search
|
61
|
+
class Scope
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
Inside your **Scope** class you must define your scope methods. You'll also be able to access to the following methods inside your scope class: **scope** and **search_attributes**.
|
66
|
+
|
67
|
+
* **`scope:`** returns the current scope when the scope method is called.
|
68
|
+
* **`search_attributes:`** returns a hash containing the all search attributes specified.
|
69
|
+
|
70
|
+
<u>**Note:**</u> All keys of **`search_attributes`** are symbolized.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
# app/searches/product_search.rb
|
74
|
+
|
75
|
+
class ProductSearch < Lupa::Search
|
76
|
+
# Scope class holds all your search methods.
|
77
|
+
class Scope
|
78
|
+
|
79
|
+
# Search method
|
80
|
+
def name
|
81
|
+
if search_attributes[:name].present?
|
82
|
+
scope.where('name LIKE ?', "%#{search_attributes[:name]}%")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Search method
|
87
|
+
def created_between
|
88
|
+
if created_start_date && created_end_date
|
89
|
+
scope.where(created_at: created_start_date..created_end_date)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Search method
|
94
|
+
def category
|
95
|
+
scope.where(category_id: search_attributes[:category])
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
# Parses search_attributes[:created_between][:start_date]
|
100
|
+
def created_start_date
|
101
|
+
search_attributes[:created_between] && search_attributes[:created_between][:start_date].try(:to_date)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Parses search_attributes[:created_between][:end_date]
|
105
|
+
def created_end_date
|
106
|
+
search_attributes[:created_between] && search_attributes[:created_between][:end_date].try(:to_date)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
The scope methods specified on the search params will be the only ones applied to the scope. Search params keys must always match the Scope class methods names.
|
112
|
+
|
113
|
+
### Public Methods
|
114
|
+
|
115
|
+
Your search class has the following public methods:
|
116
|
+
|
117
|
+
- **`scope:`** returns the scope to which all search rules will be applied.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
search = ProductSearch.new(current_user.products).search(name: 'chair', category: '23')
|
121
|
+
search.scope
|
122
|
+
|
123
|
+
# => current_user.products
|
124
|
+
```
|
125
|
+
|
126
|
+
- **`search_attributes:`** returns a hash with all search attributes including default search attributes.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
search = ProductSearch.new(current_user.products).search(name: 'chair', category: '23')
|
130
|
+
search.search_attributes
|
131
|
+
|
132
|
+
# => { name: 'chair', category: '23' }
|
133
|
+
```
|
134
|
+
- **`default_search_attributes:`** returns a hash with default search attributes. A more detailed explanation about default search attributes can be found below this section.
|
135
|
+
|
136
|
+
- **`results:`** returns the resulting scope after all searching rules have been applied.
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
search = ProductSearch.new(current_user.products).search(name: 'chair', category: '23')
|
140
|
+
search.results
|
141
|
+
|
142
|
+
# => #<Product::ActiveRecord_Relation:0x007ffda11b7d48>
|
143
|
+
```
|
144
|
+
|
145
|
+
- **OTHER METHODS** applied to your search class will result in calling to **`results`** and applying that method to the resulting scope. If the resulting scope doesn't respond to the method, an exception will be raised.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
search = ProductSearch.new(current_user.products).search(name: 'chair', category: '23')
|
149
|
+
|
150
|
+
search.first
|
151
|
+
# => #<Product id: 1, name: 'Eames Chair', category_id: 23, created_at: "2015-04-06 18:54:13", updated_at: "2015-04-06 18:54:13" >
|
152
|
+
|
153
|
+
search.unexisting_method
|
154
|
+
# => Lupa::ResultMethodNotImplementedError: The resulting scope does not respond to unexisting_method method.
|
155
|
+
```
|
156
|
+
|
157
|
+
## Default Search Scope
|
158
|
+
|
159
|
+
You can define a default search scope if you want to use a search class with an specific resource by overriding the initialize method as follows:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
# app/searches/product_search.rb
|
163
|
+
|
164
|
+
class ProductSearch < Lupa::Search
|
165
|
+
class Scope
|
166
|
+
...
|
167
|
+
end
|
168
|
+
|
169
|
+
# Be careful not to change the scope variable name,
|
170
|
+
# otherwise you will experiment some issues.
|
171
|
+
def initialize(scope = Product.all)
|
172
|
+
@scope = scope
|
173
|
+
end
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
Then you can use your search class without passing the scope:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
search = ProductSearch.search(name: 'chair', category: '23')
|
181
|
+
|
182
|
+
search.first
|
183
|
+
# => #<Product id: 1, name: 'Eames Chair', category_id: 23, created_at: "2015-04-06 18:54:13", updated_at: "2015-04-06 18:54:13" >
|
184
|
+
```
|
185
|
+
|
186
|
+
## Default Search Attributes
|
187
|
+
|
188
|
+
Defining default search attributes will cause the scope method to be invoked always.
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
# app/searches/product_search.rb
|
192
|
+
|
193
|
+
class ProductSearch < Lupa::Search
|
194
|
+
class Scope
|
195
|
+
...
|
196
|
+
end
|
197
|
+
|
198
|
+
# This should always return a hash
|
199
|
+
def default_search_attributes
|
200
|
+
{ category: 23 }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
```
|
204
|
+
**<u>Note:</u>** You can override default search attributes by passing it to the search params.
|
205
|
+
|
206
|
+
``` ruby
|
207
|
+
search = ProductSearch.new(current_user.products).search(name: 'chair', category: '42')
|
208
|
+
|
209
|
+
search.search_attributes
|
210
|
+
# => { name: 'chair', category: 42 }
|
211
|
+
```
|
212
|
+
|
213
|
+
## Combining Search Classes
|
214
|
+
|
215
|
+
You can reuse your search class in order to keep them DRY.
|
216
|
+
|
217
|
+
A common example is searching records created between two dates. So lets create a **CreatedAtSearch** class to handle that logic.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
# app/searches/created_between_search.rb
|
221
|
+
|
222
|
+
class CreatedAtSearch < Lupa::Search
|
223
|
+
class Scope
|
224
|
+
|
225
|
+
def created_before
|
226
|
+
...
|
227
|
+
end
|
228
|
+
|
229
|
+
def created_after
|
230
|
+
...
|
231
|
+
end
|
232
|
+
|
233
|
+
def created_between
|
234
|
+
if created_start_date && created_end_date
|
235
|
+
scope.where(created_at: created_start_date..created_end_date)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
def created_start_date
|
242
|
+
search_attributes[:created_between] && search_attributes[:created_between][:start_date].try(:to_date)
|
243
|
+
end
|
244
|
+
|
245
|
+
def created_end_date
|
246
|
+
search_attributes[:created_between] && search_attributes[:created_between][:end_date].try(:to_date)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
Now we can use it in our **ProductSearch** class:
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
# app/searches/product_search.rb
|
256
|
+
|
257
|
+
class ProductSearch < Lupa::Search
|
258
|
+
class Scope
|
259
|
+
|
260
|
+
def name
|
261
|
+
...
|
262
|
+
end
|
263
|
+
|
264
|
+
# We use CreatedAtSearch class to perform the search
|
265
|
+
def created_between
|
266
|
+
if search_attributes[:created_between]
|
267
|
+
CreadtedAtSearch.new(scope).search(created_between: search_attributes[:created_between])
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def category
|
272
|
+
...
|
273
|
+
end
|
274
|
+
|
275
|
+
end
|
276
|
+
end
|
277
|
+
```
|
278
|
+
|
279
|
+
## Testing
|
280
|
+
|
281
|
+
This is a list of things you should test when creating a search class:
|
282
|
+
|
283
|
+
- **Default Scope** if specified.
|
284
|
+
- **Default Search Attributes** if specified.
|
285
|
+
- **Each Scope Method** individually.
|
286
|
+
|
287
|
+
### Testing Default Scope
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
# app/searches/product_search.rb
|
291
|
+
|
292
|
+
class ProductSearch < Lupa::Search
|
293
|
+
class Scope
|
294
|
+
...
|
295
|
+
end
|
296
|
+
|
297
|
+
def initialize(scope = Product.all)
|
298
|
+
@scope = scope
|
299
|
+
end
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
# test/searches/product_search_test.rb
|
305
|
+
require 'test_helper'
|
306
|
+
|
307
|
+
describe ProductSearch do
|
308
|
+
describe 'Default Scope' do
|
309
|
+
context 'when not passing a scope to search initializer and no search params' do
|
310
|
+
it 'returns default scope' do
|
311
|
+
results = ProductSearch.search({}).results
|
312
|
+
results.must_equal Product.all
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
```
|
318
|
+
|
319
|
+
### Testing Default Search Attributes
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
# app/searches/product_search.rb
|
323
|
+
|
324
|
+
class ProductSearch < Lupa::Search
|
325
|
+
class Scope
|
326
|
+
...
|
327
|
+
end
|
328
|
+
|
329
|
+
def initialize(scope = Product.all)
|
330
|
+
@scope = scope
|
331
|
+
end
|
332
|
+
|
333
|
+
def default_search_attributes
|
334
|
+
{ category: '23' }
|
335
|
+
end
|
336
|
+
end
|
337
|
+
```
|
338
|
+
|
339
|
+
```ruby
|
340
|
+
# test/searches/product_search_test.rb
|
341
|
+
require 'test_helper'
|
342
|
+
|
343
|
+
describe ProductSearch do
|
344
|
+
describe 'Default Search Attributes' do
|
345
|
+
context 'when not overriding default_search_attributes' do
|
346
|
+
it 'returns default default_search_attributes' do
|
347
|
+
default_search_attributes = { category: 23 }
|
348
|
+
search = ProductSearch.search({})
|
349
|
+
search.default_search_attributes.must_equal default_search_attributes
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
### Testing Each Scope Method Individually
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
# app/searches/product_search.rb
|
360
|
+
|
361
|
+
class ProductSearch < Lupa::Search
|
362
|
+
class Scope
|
363
|
+
def category
|
364
|
+
scope.where(category_id: search_attributes[:category])
|
365
|
+
end
|
366
|
+
|
367
|
+
def name
|
368
|
+
...
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def initialize(scope = Product.all)
|
373
|
+
@scope = scope
|
374
|
+
end
|
375
|
+
end
|
376
|
+
```
|
377
|
+
|
378
|
+
```ruby
|
379
|
+
# test/searches/product_search_test.rb
|
380
|
+
|
381
|
+
require 'test_helper'
|
382
|
+
|
383
|
+
describe ProductSearch do
|
384
|
+
describe 'Scopes' do
|
385
|
+
|
386
|
+
describe '#category' do
|
387
|
+
it 'returns products from specified category' do
|
388
|
+
results = ProductSearch.search(category: '23').results
|
389
|
+
results.must_equal Product.where(category_id: '23')
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
describe '#name' do
|
394
|
+
it 'returns products that contain specified letters' do
|
395
|
+
...
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
end
|
400
|
+
end
|
401
|
+
```
|
402
|
+
|
403
|
+
## Installation
|
404
|
+
|
405
|
+
Add this line to your application's Gemfile:
|
406
|
+
|
407
|
+
gem 'lupa'
|
408
|
+
|
409
|
+
And then execute:
|
410
|
+
|
411
|
+
$ bundle
|
412
|
+
|
413
|
+
Or install it yourself as:
|
414
|
+
|
415
|
+
$ gem install lupa
|
416
|
+
|
417
|
+
|
418
|
+
## Contributing
|
419
|
+
|
420
|
+
1. Fork it ( https://github.com/edelpero/lupa/fork )
|
421
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
422
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
423
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
424
|
+
5. Create a new Pull Request
|