lupa 1.0.0 → 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 83dad7fde61871e8d0333b609958b6eadaccc23c
4
- data.tar.gz: 1a1eb1e14ede2f72d5f7eb029c38b2511e9182f6
2
+ SHA256:
3
+ metadata.gz: d6c3063e519871ee2848d6202a6388482747e7e2668f04122036e3a5ae8dd1fa
4
+ data.tar.gz: 7cb6850bb59992671ac66d19ef3179bb7c400dae6d39faaa7cbe5bfbdb837643
5
5
  SHA512:
6
- metadata.gz: e6f6127f879df5ec3bcee9e4c0c479df20aa53c17389fedf7a10d1403900da7920372a6bf2dab44ddad82843465c71834fe3343e515f506d63f331b4feec23b3
7
- data.tar.gz: 84e3f845e779bdb383410f32509b99b4f7ae5fcda658dbb5e3dd49e8110b735c66b47c49ff583a2942822df800fc656fe8315c2f52bdb3b6807e476a5c7b06e0
6
+ metadata.gz: 4b41e376873605d430296381df17225bf5d168b885857be7a5b395d206b4a7ab4f388473928898003539df96bbb64e6d4fc02c130914c5245b6adf0d3c2034ca
7
+ data.tar.gz: 3003501628ef7c228bf4762bdbaf9f81cb00b2b6e436f75940adfa710e1a2ffd30224b6a3155a828c1fa942a1ca1589ae7e9fbf4056c83dede6b0622381a423e
@@ -0,0 +1,56 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ ruby-version:
16
+ - '2.2'
17
+ - '2.3'
18
+ - '2.4'
19
+ - '2.5'
20
+ - '2.6'
21
+ - '2.7'
22
+ - '3.0'
23
+ - '3.1'
24
+ - '3.2'
25
+ - '3.3'
26
+
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+
30
+ - name: Set up Ruby ${{ matrix.ruby-version }}
31
+ uses: ruby/setup-ruby@v1
32
+ with:
33
+ ruby-version: ${{ matrix.ruby-version }}
34
+ bundler-cache: true
35
+
36
+ - name: Run tests
37
+ run: bundle exec rake test
38
+
39
+ - name: Upload coverage to Coveralls
40
+ if: ${{ matrix.ruby-version >= '2.5' }}
41
+ uses: coverallsapp/github-action@v2
42
+ with:
43
+ github-token: ${{ secrets.GITHUB_TOKEN }}
44
+ flag-name: ruby-${{ matrix.ruby-version }}
45
+ parallel: true
46
+
47
+ coveralls-finish:
48
+ needs: test
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - name: Coveralls Finished
52
+ uses: coverallsapp/github-action@v2
53
+ with:
54
+ github-token: ${{ secrets.GITHUB_TOKEN }}
55
+ parallel-finished: true
56
+
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.5
6
+ - 2.2.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ ## 1.0.2
2
+
3
+ * documentation
4
+ * Add comprehensive RDoc documentation for all classes and methods
5
+ * Include detailed examples and usage patterns in RDoc
6
+ * Document all error classes with practical examples
7
+ * Fix multiple typos in README.md
8
+ * Update Table of Contents links in README.md
9
+ * Add benchmarks section comparing Lupa with HasScope and Searchlight
10
+ * Improve code examples and usage patterns in README.md
11
+
12
+ * ci
13
+ * Migrate from Travis CI to GitHub Actions
14
+ * Add support for Ruby 2.2 through 3.3
15
+ * Configure Coveralls for Ruby 2.5+ (conditional loading for compatibility)
16
+
17
+ * dependencies
18
+ * Update minitest development dependency from ~> 5.5.1 to ~> 5.5
19
+ * Update bundler development dependency from ~> 1.6 to >= 1.6
20
+ * Add rake development dependency >= 10.0
21
+
22
+ ## 1.0.1
23
+
24
+ * enhancements
25
+ * A **Lupa::DefaultSearchAttributesError** exception will be raised if `default_search_attributes` does not return a hash.
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in lupa.gemspec
4
4
  gemspec
5
+
6
+ gem 'coveralls', require: false
data/README.md CHANGED
@@ -2,53 +2,66 @@
2
2
 
3
3
  Lupa means *Magnifier* in spanish.
4
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.
5
+ [![CI](https://github.com/edelpero/lupa/actions/workflows/ci.yml/badge.svg)](https://github.com/edelpero/lupa/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/edelpero/lupa/badge.svg?branch=master)](https://coveralls.io/r/edelpero/lupa?branch=master) [![Code Climate](https://codeclimate.com/github/edelpero/lupa/badges/gpa.svg)](https://codeclimate.com/github/edelpero/lupa) [![Inline docs](http://inch-ci.org/github/edelpero/lupa.svg?branch=master)](http://inch-ci.org/github/edelpero/lupa)
6
6
 
7
- ## Search Class
7
+ Lupa lets you create simple, robust and scaleable search filters with ease using regular Ruby classes and object oriented design patterns.
8
8
 
9
- ### Usage
9
+ Lupa is Framework and ORM agnostic. It will work with any ORM or Object that can build a query using **chained method calls**, like ActiveRecord: `
10
+ Product.where(name: 'Digital').where(category: '23').limit(2)`.
10
11
 
11
- The example will explain how to use the class on a Rails application:
12
+ **Table of Contents:**
12
13
 
13
- - Define a custom form:
14
+ * [Search Class](#search-class)
15
+ * [Overview](#overview)
16
+ * [Definition](#definition)
17
+ * [Public Methods](#public-methods)
18
+ * [Default Search Scope](#default-search-scope)
19
+ * [Default Search Attributes](#default-search-attributes)
20
+ * [Combining Search Classes](#combining-search-classes)
21
+ * [Usage with Rails](#usage-with-rails)
22
+ * [Testing](#testing)
23
+ * [Testing Default Scope](#testing-default-scope)
24
+ * [Testing Default Search Attributes](#testing-default-search-attributes)
25
+ * [Testing Each Scope Method Individually](#testing-each-scope-method-individually)
26
+ * [Benchmarks](#benchmarks)
27
+ * [Lupa vs HasScope](#lupa-vs-hasscope)
28
+ * [Lupa vs Searchlight](#lupa-vs-searchlight)
29
+ * [Installation](#installation)
14
30
 
15
- ```haml
16
- # app/views/products/_search.html.haml
17
31
 
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:
32
+ ## Search Class
33
+
34
+ ### Overview
26
35
 
27
36
  ```ruby
28
- # app/controllers/products_controller.rb
37
+ products = ProductSearch.new(current_user.products).search(name: 'digital', category: '23')
29
38
 
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
+ # Iterate over the search results
40
+ products.each do |product|
41
+ # Your logic goes here
39
42
  end
40
43
  ```
41
- - Loop through the search results on your view.
44
+ Calling **.each** on the instance will build a search by chaining calls to **name** and **category** methods defined in our **ProductSearch::Scope** class.
42
45
 
43
- ```haml
44
- # app/views/products/index.html.haml
46
+ ```ruby
47
+ # app/searches/product_search.rb
45
48
 
46
- %h1 Products
49
+ class ProductSearch < Lupa::Search
50
+ # Scope class holds all your search methods.
51
+ class Scope
47
52
 
48
- %ul
49
- - @products.each do |product|
50
- %li
51
- = "#{product.name} - #{product.price} - #{product.category}"
53
+ # Search method
54
+ def name
55
+ scope.where('name iLIKE ?', "%#{search_attributes[:name]}%")
56
+ end
57
+
58
+ # Search method
59
+ def category
60
+ scope.where(category_id: search_attributes[:category])
61
+ end
62
+
63
+ end
64
+ end
52
65
  ```
53
66
 
54
67
  ### Definition
@@ -65,7 +78,7 @@ end
65
78
  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
79
 
67
80
  * **`scope:`** returns the current scope when the scope method is called.
68
- * **`search_attributes:`** returns a hash containing the all search attributes specified.
81
+ * **`search_attributes:`** returns a hash containing the all search attributes specified including the default ones.
69
82
 
70
83
  <u>**Note:**</u> All keys of **`search_attributes`** are symbolized.
71
84
 
@@ -75,36 +88,17 @@ Inside your **Scope** class you must define your scope methods. You'll also be a
75
88
  class ProductSearch < Lupa::Search
76
89
  # Scope class holds all your search methods.
77
90
  class Scope
78
-
91
+
79
92
  # Search method
80
93
  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
94
+ scope.where('name LIKE ?', "%#{search_attributes[:name]}%")
91
95
  end
92
-
96
+
93
97
  # Search method
94
98
  def category
95
- scope.where(category_id: search_attributes[:category])
99
+ scope.where(category_id: search_attributes[:category])
96
100
  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
101
+
108
102
  end
109
103
  end
110
104
  ```
@@ -154,7 +148,7 @@ search.unexisting_method
154
148
  # => Lupa::ResultMethodNotImplementedError: The resulting scope does not respond to unexisting_method method.
155
149
  ```
156
150
 
157
- ## Default Search Scope
151
+ ### Default Search Scope
158
152
 
159
153
  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
154
 
@@ -165,9 +159,9 @@ class ProductSearch < Lupa::Search
165
159
  class Scope
166
160
  ...
167
161
  end
168
-
162
+
169
163
  # Be careful not to change the scope variable name,
170
- # otherwise you will experiment some issues.
164
+ # otherwise you will experience issues.
171
165
  def initialize(scope = Product.all)
172
166
  @scope = scope
173
167
  end
@@ -183,7 +177,7 @@ search.first
183
177
  # => #<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
178
  ```
185
179
 
186
- ## Default Search Attributes
180
+ ### Default Search Attributes
187
181
 
188
182
  Defining default search attributes will cause the scope method to be invoked always.
189
183
 
@@ -194,23 +188,31 @@ class ProductSearch < Lupa::Search
194
188
  class Scope
195
189
  ...
196
190
  end
197
-
191
+
198
192
  # This should always return a hash
199
193
  def default_search_attributes
200
- { category: 23 }
194
+ { category: '23' }
201
195
  end
202
196
  end
203
197
  ```
198
+
199
+ ```ruby
200
+ search = ProductSearch.new(current_user.products).search(name: 'chair')
201
+
202
+ search.search_attributes
203
+ # => { name: 'chair', category: '23' }
204
+ ```
205
+
204
206
  **<u>Note:</u>** You can override default search attributes by passing it to the search params.
205
207
 
206
208
  ``` ruby
207
209
  search = ProductSearch.new(current_user.products).search(name: 'chair', category: '42')
208
210
 
209
211
  search.search_attributes
210
- # => { name: 'chair', category: 42 }
212
+ # => { name: 'chair', category: '42' }
211
213
  ```
212
214
 
213
- ## Combining Search Classes
215
+ ### Combining Search Classes
214
216
 
215
217
  You can reuse your search class in order to keep them DRY.
216
218
 
@@ -221,29 +223,33 @@ A common example is searching records created between two dates. So lets create
221
223
 
222
224
  class CreatedAtSearch < Lupa::Search
223
225
  class Scope
224
-
226
+
225
227
  def created_before
226
228
  ...
227
229
  end
228
-
230
+
229
231
  def created_after
230
232
  ...
231
233
  end
232
-
234
+
233
235
  def created_between
234
236
  if created_start_date && created_end_date
235
237
  scope.where(created_at: created_start_date..created_end_date)
236
238
  end
237
239
  end
238
-
240
+
239
241
  private
240
242
 
243
+ # Parses search_attributes[:created_between][:start_date]
241
244
  def created_start_date
242
- search_attributes[:created_between] && search_attributes[:created_between][:start_date].try(:to_date)
245
+ search_attributes[:created_between] &&
246
+ search_attributes[:created_between][:start_date].try(:to_date)
243
247
  end
244
-
248
+
249
+ # Parses search_attributes[:created_between][:end_date]
245
250
  def created_end_date
246
- search_attributes[:created_between] && search_attributes[:created_between][:end_date].try(:to_date)
251
+ search_attributes[:created_between] &&
252
+ search_attributes[:created_between][:end_date].try(:to_date)
247
253
  end
248
254
  end
249
255
  end
@@ -256,25 +262,78 @@ Now we can use it in our **ProductSearch** class:
256
262
 
257
263
  class ProductSearch < Lupa::Search
258
264
  class Scope
259
-
265
+
260
266
  def name
261
267
  ...
262
268
  end
263
-
264
- # We use CreatedAtSearch class to perform the search
269
+
270
+ # We use CreatedAtSearch class to perform the search.
271
+ # Be sure to always call `results` method on your composed
272
+ # search class.
265
273
  def created_between
266
- if search_attributes[:created_between]
267
- CreadtedAtSearch.new(scope).search(created_between: search_attributes[:created_between])
268
- end
274
+ CreatedAtSearch.new(scope).
275
+ search(created_between: search_attributes[:created_between]).
276
+ results
269
277
  end
270
-
278
+
271
279
  def category
272
280
  ...
273
281
  end
274
-
282
+
275
283
  end
276
284
  end
277
285
  ```
286
+ **Note:** If you are combining search classes. Be sure to always call **results** method on the search classes composing your main search class.
287
+
288
+ ## Usage with Rails
289
+
290
+ ### Forms
291
+
292
+ Define a custom form:
293
+
294
+ ```haml
295
+ # app/views/products/_search.html.haml
296
+
297
+ = form_tag products_path, method: :get do
298
+ = text_field_tag 'name'
299
+ = select_tag 'category', options_from_collection_for_select(@categories, 'id', 'name')
300
+ = date_field_tag 'created_between[start_date]'
301
+ = date_field_tag 'created_between[end_date]'
302
+ = submit_tag :search
303
+ ```
304
+
305
+ ### Controllers
306
+
307
+ 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:
308
+
309
+ ```ruby
310
+ # app/controllers/products_controller.rb
311
+
312
+ class ProductsController < ApplicationController
313
+ def index
314
+ @products = ProductSearch.new(current_user.products).search(search_params)
315
+ end
316
+
317
+ protected
318
+ def search_params
319
+ params.permit(:name, :category, created_between: [:start_date, :end_date])
320
+ end
321
+ end
322
+ ```
323
+ ### Views
324
+
325
+ Loop through the search results on your view.
326
+
327
+ ```haml
328
+ # app/views/products/index.html.haml
329
+
330
+ %h1 Products
331
+
332
+ %ul
333
+ - @products.each do |product|
334
+ %li
335
+ = "#{product.name} - #{product.price} - #{product.category}"
336
+ ```
278
337
 
279
338
  ## Testing
280
339
 
@@ -400,6 +459,42 @@ describe ProductSearch do
400
459
  end
401
460
  ```
402
461
 
462
+ ## Benchmarks
463
+
464
+ I used [benchmark-ips](https://github.com/evanphx/benchmark-ips).
465
+
466
+ ### Lupa vs. [HasScope](https://github.com/plataformatec/has_scope)
467
+
468
+ ```
469
+ Calculating -------------------------------------
470
+ lupa 265.000 i/100ms
471
+ has_scope 254.000 i/100ms
472
+ -------------------------------------------------
473
+ lupa 3.526k (±24.7%) i/s - 67.045k
474
+ has_scope 3.252k (±24.8%) i/s - 61.976k
475
+
476
+ Comparison:
477
+ lupa: 3525.8 i/s
478
+ has_scope: 3252.0 i/s - 1.08x slower
479
+ ```
480
+
481
+ ### Lupa vs. [Searchlight](https://github.com/nathanl/searchlight)
482
+
483
+ ```
484
+ Calculating -------------------------------------
485
+ lupa 480.000 i/100ms
486
+ searchlight 232.000 i/100ms
487
+ -------------------------------------------------
488
+ lupa 7.273k (±25.1%) i/s - 689.280k
489
+ searchlight 2.665k (±14.1%) i/s - 260.072k
490
+
491
+ Comparison:
492
+ lupa: 7273.5 i/s
493
+ searchlight: 2665.4 i/s - 2.73x slower
494
+ ```
495
+
496
+ *If you know about another gem that was not included on the benchmark, feel free to run the benchmarks and send a Pull Request.*
497
+
403
498
  ## Installation
404
499
 
405
500
  Add this line to your application's Gemfile:
data/Rakefile CHANGED
@@ -8,4 +8,4 @@ Rake::TestTask.new do |t|
8
8
  t.verbose = true
9
9
  end
10
10
 
11
-
11
+ task :default => :test
@@ -1,13 +1,90 @@
1
1
  module Lupa
2
+ # Internal module that provides common functionality to Scope classes.
3
+ # This module is automatically included in the Scope class defined within
4
+ # your search class.
5
+ #
6
+ # It provides access to two key attributes:
7
+ # - `scope`: The current scope being searched
8
+ # - `search_attributes`: The hash of search parameters
9
+ #
10
+ # @example Accessing scope and search_attributes in a Scope class
11
+ # class ProductSearch < Lupa::Search
12
+ # class Scope
13
+ # # scope and search_attributes are available here
14
+ # def name
15
+ # # scope is the current ActiveRecord relation (or any chainable object)
16
+ # scope.where('name LIKE ?', "%#{search_attributes[:name]}%")
17
+ # end
18
+ #
19
+ # def price_range
20
+ # # search_attributes contains all search parameters
21
+ # if search_attributes[:price_range]
22
+ # scope.where(price: search_attributes[:price_range])
23
+ # else
24
+ # scope
25
+ # end
26
+ # end
27
+ # end
28
+ # end
29
+ #
30
+ # @note This module is included automatically by Lupa and should not be
31
+ # included manually in your code.
32
+ #
33
+ # @api private
34
+ # @since 0.1.0
2
35
  module ScopeMethods
3
-
36
+ # @!attribute [rw] scope
37
+ # The current scope object that search methods will operate on.
38
+ # This is typically an ActiveRecord::Relation or similar chainable object.
39
+ # The scope is updated after each search method is called.
40
+ #
41
+ # @return [Object] the current scope object
42
+ #
43
+ # @example Accessing the scope in a search method
44
+ # def category
45
+ # # scope is the current state of the query chain
46
+ # scope.where(category_id: search_attributes[:category])
47
+ # end
4
48
  attr_accessor :scope
5
- attr_reader :search_attributes
6
49
 
50
+ # @!attribute [r] search_attributes
51
+ # A hash containing all search attributes, including default search attributes.
52
+ # All keys are symbolized automatically by Lupa.
53
+ #
54
+ # @return [Hash] the search attributes hash with symbolized keys
55
+ #
56
+ # @example Accessing search attributes in a scope method
57
+ # def search_by_name
58
+ # name = search_attributes[:name]
59
+ # scope.where('name LIKE ?', "%#{name}%") if name.present?
60
+ # end
61
+ #
62
+ # @example With nested hash attributes
63
+ # def created_between
64
+ # start_date = search_attributes[:created_between][:start_date]
65
+ # end_date = search_attributes[:created_between][:end_date]
66
+ # scope.where(created_at: start_date..end_date)
67
+ # end
68
+ attr_reader :search_attributes
69
+
70
+ # Initializes a new Scope instance with the given scope and search attributes.
71
+ # This method is called automatically by Lupa::Search and should not be called directly.
72
+ #
73
+ # @param scope [Object] the initial scope object to search on (e.g., ActiveRecord::Relation)
74
+ # @param search_attributes [Hash] the hash of search parameters with symbolized keys
75
+ #
76
+ # @return [ScopeMethods] the initialized scope instance
77
+ #
78
+ # @example Internal usage (automatically called by Lupa)
79
+ # # This happens internally when you call:
80
+ # ProductSearch.new(Product.all).search(name: 'chair')
81
+ # # Lupa automatically calls:
82
+ # ProductSearch::Scope.new(Product.all, { name: 'chair' })
83
+ #
84
+ # @api private
7
85
  def initialize(scope, search_attributes)
8
86
  @scope = scope
9
87
  @search_attributes = search_attributes
10
88
  end
11
-
12
89
  end
13
90
  end