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
data/Rakefile
ADDED
data/lib/lupa.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "lupa/version"
|
2
|
+
|
3
|
+
module Lupa
|
4
|
+
DefaultScopeError = Class.new(StandardError)
|
5
|
+
ScopeMethodNotImplementedError = Class.new(NotImplementedError)
|
6
|
+
ResultMethodNotImplementedError = Class.new(NotImplementedError)
|
7
|
+
SearchAttributesError = Class.new(StandardError)
|
8
|
+
end
|
9
|
+
|
10
|
+
require "lupa/scope_methods"
|
11
|
+
require "lupa/search"
|
data/lib/lupa/search.rb
ADDED
@@ -0,0 +1,390 @@
|
|
1
|
+
module Lupa
|
2
|
+
class Search
|
3
|
+
class Scope; end
|
4
|
+
|
5
|
+
# Public: Return class scope.
|
6
|
+
#
|
7
|
+
# === Examples
|
8
|
+
#
|
9
|
+
# class ProductSearch < Lupa::Search
|
10
|
+
#
|
11
|
+
# class Scope
|
12
|
+
#
|
13
|
+
# def category
|
14
|
+
# scope.where(category: search_attributes[:category])
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# def in_stock
|
18
|
+
# scope.where(in_stock: search_attributes[:in_stock])
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def default_search_attributes
|
24
|
+
# { in_stock: true }
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# search = ProductSearch.new(@products).search({ category: 'furniture' })
|
30
|
+
# search.scope
|
31
|
+
# # => @products
|
32
|
+
#
|
33
|
+
# Returns your class scope.
|
34
|
+
attr_reader :scope
|
35
|
+
|
36
|
+
# Public: Return class search attributes including default search attributes.
|
37
|
+
#
|
38
|
+
# === Examples
|
39
|
+
#
|
40
|
+
# class ProductSearch < Lupa::Search
|
41
|
+
#
|
42
|
+
# class Scope
|
43
|
+
#
|
44
|
+
# def category
|
45
|
+
# scope.where(category: search_attributes[:category])
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# def in_stock
|
49
|
+
# scope.where(in_stock: search_attributes[:in_stock])
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# def default_search_attributes
|
55
|
+
# { in_stock: true }
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# search = ProductSearch.new(@products).search({ category: 'furniture' })
|
61
|
+
# search.search_attributes
|
62
|
+
# # => { category: furniture, in_stock: true }
|
63
|
+
#
|
64
|
+
# Returns your class search attributes including default search attributes.
|
65
|
+
attr_reader :search_attributes
|
66
|
+
|
67
|
+
# Public: Create a new instance of the class.
|
68
|
+
#
|
69
|
+
# === Options
|
70
|
+
#
|
71
|
+
# <tt>scope</tt> - An object which will be use to perform all the search operations.
|
72
|
+
#
|
73
|
+
# === Examples
|
74
|
+
#
|
75
|
+
# class ProductSearch < Lupa::Search
|
76
|
+
#
|
77
|
+
# class Scope
|
78
|
+
#
|
79
|
+
# def category
|
80
|
+
# scope.where(category: search_attributes[:category])
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# def in_stock
|
84
|
+
# scope.where(in_stock: search_attributes[:in_stock])
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# def default_search_attributes
|
90
|
+
# { in_stock: true }
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# scope = Product.where(price: 20..30)
|
96
|
+
# search = ProductSearch.new(scope)
|
97
|
+
#
|
98
|
+
# Returns a new instance of the class.
|
99
|
+
def initialize(scope)
|
100
|
+
@scope = scope
|
101
|
+
end
|
102
|
+
|
103
|
+
# Public: Return default a hash containing default search attributes of the class.
|
104
|
+
#
|
105
|
+
# === Examples
|
106
|
+
#
|
107
|
+
# class ProductSearch < Lupa::Search
|
108
|
+
#
|
109
|
+
# class Scope
|
110
|
+
#
|
111
|
+
# def category
|
112
|
+
# scope.where(category: search_attributes[:category])
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# def in_stock
|
116
|
+
# scope.where(in_stock: search_attributes[:in_stock])
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# def default_search_attributes
|
122
|
+
# { in_stock: true }
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# scope = Product.where(price: 20..30)
|
128
|
+
# search = ProductSearch.new(scope).search({ category: 'furniture' })
|
129
|
+
# search.default_search_attributes
|
130
|
+
# # => { in_stock: true }
|
131
|
+
#
|
132
|
+
# Returns default a hash containing default search attributes of the class.
|
133
|
+
def default_search_attributes
|
134
|
+
{}
|
135
|
+
end
|
136
|
+
|
137
|
+
# Public: Set and checks search attributes, and instantiates the Scope class.
|
138
|
+
#
|
139
|
+
# === Options
|
140
|
+
#
|
141
|
+
# <tt>attributes</tt> - The hash containing the search attributes.
|
142
|
+
#
|
143
|
+
# * If attributes is not a Hash kind of class, it will raise a
|
144
|
+
# Lupa::SearchAttributesError.
|
145
|
+
# * If attributes keys don't match methods
|
146
|
+
# defined on your class, it will raise a Lupa::NotImplementedError.
|
147
|
+
#
|
148
|
+
# === Examples
|
149
|
+
#
|
150
|
+
# class ProductSearch < Lupa::Search
|
151
|
+
#
|
152
|
+
# class Scope
|
153
|
+
#
|
154
|
+
# def category
|
155
|
+
# scope.where(category: search_attributes[:category])
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
# scope = Product.where(price: 20..30)
|
163
|
+
# search = ProductSearch.new(scope).search({ category: 'furniture' })
|
164
|
+
# # => #<ProductSearch:0x007f7f74070850 @scope=scope, @search_attributes={:category=>'furniture'}, @scope_class=#<ProductSearch::Scope:0x007fd2811001e8 @scope=[1, 2, 3, 4, 5, 6, 7, 8], @search_attributes={:even_numbers=>true}>>
|
165
|
+
#
|
166
|
+
# Returns the class instance itself.
|
167
|
+
def search(attributes)
|
168
|
+
raise Lupa::SearchAttributesError, "Your search params needs to be a hash." unless attributes.respond_to?(:keys)
|
169
|
+
|
170
|
+
set_search_attributes(attributes)
|
171
|
+
set_scope_class
|
172
|
+
check_method_definitions
|
173
|
+
self
|
174
|
+
end
|
175
|
+
|
176
|
+
# Public: Creates a new instance of the search class an applies search method with attributes to it.
|
177
|
+
#
|
178
|
+
# === Options
|
179
|
+
#
|
180
|
+
# <tt>attributes</tt> - The hash containing the search attributes.
|
181
|
+
#
|
182
|
+
# * If search class doesn't have a default scope specified, it will raise a
|
183
|
+
# Lupa::DefaultScopeError exception.
|
184
|
+
# * If attributes is not a Hash kind of class, it will raise a
|
185
|
+
# Lupa::SearchAttributesError exception.
|
186
|
+
# * If attributes keys don't match methods
|
187
|
+
# defined on your class, it will raise a Lupa::NotImplementedError.
|
188
|
+
#
|
189
|
+
# === Examples
|
190
|
+
#
|
191
|
+
# class ProductSearch < Lupa::Search
|
192
|
+
#
|
193
|
+
# class Scope
|
194
|
+
#
|
195
|
+
# def category
|
196
|
+
# scope.where(category: search_attributes[:category])
|
197
|
+
# end
|
198
|
+
#
|
199
|
+
# end
|
200
|
+
#
|
201
|
+
# def initialize(scope = Product.in_stock)
|
202
|
+
# @scope = scope
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# end
|
206
|
+
#
|
207
|
+
# search = ProductSearch.search({ category: 'furniture' })
|
208
|
+
# # => #<ProductSearch:0x007f7f74070850 @scope=scope, @search_attributes={:category=>'furniture'}, @scope_class=#<ProductSearch::Scope:0x007fd2811001e8 @scope=[1, 2, 3, 4, 5, 6, 7, 8], @search_attributes={:even_numbers=>true}>>
|
209
|
+
#
|
210
|
+
# Returns the class instance itself.
|
211
|
+
def self.search(attributes)
|
212
|
+
new.search(attributes)
|
213
|
+
rescue ArgumentError
|
214
|
+
raise Lupa::DefaultScopeError, "You need to define a default scope in order to user search class method."
|
215
|
+
end
|
216
|
+
|
217
|
+
# Public: Return the search result.
|
218
|
+
#
|
219
|
+
# === Examples
|
220
|
+
#
|
221
|
+
# class ProductSearch < Lupa::Search
|
222
|
+
#
|
223
|
+
# class Scope
|
224
|
+
#
|
225
|
+
# def category
|
226
|
+
# scope.where(category: search_attributes[:category])
|
227
|
+
# end
|
228
|
+
#
|
229
|
+
# end
|
230
|
+
#
|
231
|
+
# def initialize(scope = Product.in_stock)
|
232
|
+
# @scope = scope
|
233
|
+
# end
|
234
|
+
#
|
235
|
+
# end
|
236
|
+
#
|
237
|
+
# search = ProductSearch.search({ category: 'furniture' }).results
|
238
|
+
# # => #<Product::ActiveRecord_Relation:0x007ffda11b7d48>
|
239
|
+
#
|
240
|
+
# Returns the search result.
|
241
|
+
def results
|
242
|
+
@results ||= run
|
243
|
+
end
|
244
|
+
|
245
|
+
# Public: Apply the missing method to the search result.
|
246
|
+
#
|
247
|
+
# === Examples
|
248
|
+
#
|
249
|
+
# class ProductSearch < Lupa::Search
|
250
|
+
#
|
251
|
+
# class Scope
|
252
|
+
#
|
253
|
+
# def category
|
254
|
+
# scope.where(category: search_attributes[:category])
|
255
|
+
# end
|
256
|
+
#
|
257
|
+
# end
|
258
|
+
#
|
259
|
+
# def initialize(scope = Product.in_stock)
|
260
|
+
# @scope = scope
|
261
|
+
# end
|
262
|
+
#
|
263
|
+
# end
|
264
|
+
#
|
265
|
+
# search = ProductSearch.search({ category: 'furniture' }).first
|
266
|
+
# # => #<Product:0x007f9c0ce1b1a8>
|
267
|
+
#
|
268
|
+
# Returns the search result.
|
269
|
+
def method_missing(method_sym, *arguments, &block)
|
270
|
+
if results.respond_to?(method_sym)
|
271
|
+
results.send(method_sym, *arguments, &block)
|
272
|
+
else
|
273
|
+
raise Lupa::ResultMethodNotImplementedError, "The resulting scope does not respond to #{method_sym} method."
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
# Internal: Store the scope class.
|
279
|
+
#
|
280
|
+
# Stores the scope class.
|
281
|
+
attr_accessor :scope_class
|
282
|
+
|
283
|
+
# Internal: Set @search_attributes by merging default search attributes with the ones passed to search method.
|
284
|
+
#
|
285
|
+
# === Options
|
286
|
+
#
|
287
|
+
# <tt>attributes</tt> - The hash containing the search attributes.
|
288
|
+
#
|
289
|
+
# === Examples
|
290
|
+
#
|
291
|
+
# class ProductSearch < Lupa::Search
|
292
|
+
#
|
293
|
+
# class Scope
|
294
|
+
#
|
295
|
+
# def category
|
296
|
+
# scope.where(category: search_attributes[:category])
|
297
|
+
# end
|
298
|
+
#
|
299
|
+
# def in_stock
|
300
|
+
# scope.where(in_stock: search_attributes[:in_stock])
|
301
|
+
# end
|
302
|
+
#
|
303
|
+
# end
|
304
|
+
#
|
305
|
+
# def default_search_attributes
|
306
|
+
# { in_stock: true }
|
307
|
+
# end
|
308
|
+
#
|
309
|
+
# scope = Product.where(in_warehouse: true)
|
310
|
+
# search = ProductSearch.new(scope).search(category: 'furniture')
|
311
|
+
#
|
312
|
+
# set_search_attributes(category: 'furniture')
|
313
|
+
# # => { category: 'furniture', in_stock: true }
|
314
|
+
#
|
315
|
+
# Sets @search_attributes by merging default search attributes with the ones passed to search method.
|
316
|
+
def set_search_attributes(attributes)
|
317
|
+
attributes = symbolize_keys(attributes)
|
318
|
+
attributes = remove_blank_attributes(attributes)
|
319
|
+
@search_attributes = default_search_attributes.merge(attributes)
|
320
|
+
end
|
321
|
+
|
322
|
+
# Internal: Symbolizes all keys passed to the search attributes.
|
323
|
+
def symbolize_keys(attributes)
|
324
|
+
return attributes.reduce({}) do |attribute, (key, value)|
|
325
|
+
attribute.tap { |a| a[key.to_sym] = symbolize_keys(value) }
|
326
|
+
end if attributes.is_a? Hash
|
327
|
+
|
328
|
+
return attributes.reduce([]) do |attribute, value|
|
329
|
+
attribute << symbolize_keys(value); attribute
|
330
|
+
end if attributes.is_a? Array
|
331
|
+
|
332
|
+
attributes
|
333
|
+
end
|
334
|
+
|
335
|
+
# Internal: Removes all empty values passed to search attributes.
|
336
|
+
def remove_blank_attributes(attributes)
|
337
|
+
attributes.delete_if { |key, value| clean_attribute(value) }
|
338
|
+
end
|
339
|
+
|
340
|
+
# Internal: Iterates over value child attributes to remove empty values.
|
341
|
+
def clean_attribute(value)
|
342
|
+
if value.kind_of?(Hash)
|
343
|
+
value.delete_if { |key, value| clean_attribute(value) }.empty?
|
344
|
+
elsif value.kind_of?(Array)
|
345
|
+
value.delete_if { |value| clean_attribute(value) }.empty?
|
346
|
+
else
|
347
|
+
value.to_s.strip.empty?
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# Internal: Includes ScopeMethods module into the Scope class and instantiate it.
|
352
|
+
def set_scope_class
|
353
|
+
klass = self.class::Scope
|
354
|
+
klass.send(:include, ScopeMethods)
|
355
|
+
@scope_class = klass.new(@scope, @search_attributes)
|
356
|
+
end
|
357
|
+
|
358
|
+
# Internal: Check for search methods to be correctly defined using search attributes.
|
359
|
+
#
|
360
|
+
# * If you pass a search attribute that doesn't exist and your Scope class
|
361
|
+
# doesn't have that method defined a Lupa::ScopeMethodNotImplementedError
|
362
|
+
# exception will be raised.
|
363
|
+
def check_method_definitions
|
364
|
+
method_names = search_attributes.keys
|
365
|
+
|
366
|
+
method_names.each do |method_name|
|
367
|
+
next if scope_class.respond_to?(method_name)
|
368
|
+
raise Lupa::ScopeMethodNotImplementedError, "#{method_name} is not defined on your #{self.class}::Scope class."
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Internal: Applies search attributes keys as methods over the scope_class.
|
373
|
+
#
|
374
|
+
# * If search_attributes are not specified a Lupa::SearchAttributesError
|
375
|
+
# exception will be raised.
|
376
|
+
#
|
377
|
+
# Returns the result of the search.
|
378
|
+
def run
|
379
|
+
raise Lupa::SearchAttributesError, "You need to specify search attributes." unless search_attributes
|
380
|
+
|
381
|
+
search_attributes.each do |method_name, value|
|
382
|
+
new_scope = scope_class.public_send(method_name)
|
383
|
+
scope_class.scope = new_scope unless new_scope.nil?
|
384
|
+
end
|
385
|
+
|
386
|
+
scope_class.scope
|
387
|
+
end
|
388
|
+
|
389
|
+
end
|
390
|
+
end
|
data/lib/lupa/version.rb
ADDED
data/lupa.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'lupa/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "lupa"
|
8
|
+
spec.version = Lupa::VERSION
|
9
|
+
spec.authors = ["Ezequiel Delpero"]
|
10
|
+
spec.email = ["edelpero@gmail.com"]
|
11
|
+
spec.summary = %q{Search Filters using Object Oriented Design.}
|
12
|
+
spec.description = %q{Lupa lets you create simple, robust and scaleable search filters with ease using regular Ruby classes and object oriented design patterns.}
|
13
|
+
spec.homepage = "https://github.com/edelpero/lupa"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "minitest", "~> 5.5.1"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
end
|