lupa 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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