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 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