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.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.test_files = FileList['test/*_test.rb']
8
+ t.verbose = true
9
+ end
10
+
11
+
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"
@@ -0,0 +1,13 @@
1
+ module Lupa
2
+ module ScopeMethods
3
+
4
+ attr_accessor :scope
5
+ attr_reader :search_attributes
6
+
7
+ def initialize(scope, search_attributes)
8
+ @scope = scope
9
+ @search_attributes = search_attributes
10
+ end
11
+
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Lupa
2
+ VERSION = "1.0.0"
3
+ end
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