lupa 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in lupa.gemspec
4
+ gemspec
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
+ ![](lupa.png)
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