search_object 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG.md +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +283 -0
  8. data/Rakefile +6 -0
  9. data/example/.gitignore +16 -0
  10. data/example/.rspec +1 -0
  11. data/example/Gemfile +11 -0
  12. data/example/README.md +34 -0
  13. data/example/Rakefile +6 -0
  14. data/example/app/assets/javascripts/application.js +5 -0
  15. data/example/app/assets/stylesheets/application.css.scss +40 -0
  16. data/example/app/assets/stylesheets/reset.css +43 -0
  17. data/example/app/controllers/application_controller.rb +3 -0
  18. data/example/app/controllers/posts_controller.rb +5 -0
  19. data/example/app/models/.keep +0 -0
  20. data/example/app/models/post.rb +13 -0
  21. data/example/app/models/post_search.rb +44 -0
  22. data/example/app/models/user.rb +5 -0
  23. data/example/app/views/layouts/application.html.slim +12 -0
  24. data/example/app/views/posts/index.html.slim +48 -0
  25. data/example/bin/bundle +3 -0
  26. data/example/bin/rails +4 -0
  27. data/example/bin/rake +4 -0
  28. data/example/config.ru +4 -0
  29. data/example/config/application.rb +27 -0
  30. data/example/config/boot.rb +4 -0
  31. data/example/config/database.yml +12 -0
  32. data/example/config/environment.rb +5 -0
  33. data/example/config/environments/development.rb +29 -0
  34. data/example/config/environments/test.rb +37 -0
  35. data/example/config/initializers/filter_parameter_logging.rb +4 -0
  36. data/example/config/initializers/secret_token.rb +12 -0
  37. data/example/config/initializers/session_store.rb +3 -0
  38. data/example/config/initializers/wrap_parameters.rb +14 -0
  39. data/example/config/routes.rb +3 -0
  40. data/example/db/migrate/20131102130117_create_users.rb +10 -0
  41. data/example/db/migrate/20131102130413_create_posts.rb +18 -0
  42. data/example/db/schema.rb +40 -0
  43. data/example/db/seeds.rb +37 -0
  44. data/example/log/.keep +0 -0
  45. data/example/screenshot.png +0 -0
  46. data/example/spec/models/post_search_spec.rb +81 -0
  47. data/example/spec/spec_helper.rb +19 -0
  48. data/lib/search_object.rb +20 -0
  49. data/lib/search_object/base.rb +64 -0
  50. data/lib/search_object/helper.rb +36 -0
  51. data/lib/search_object/plugin/kaminari.rb +18 -0
  52. data/lib/search_object/plugin/model.rb +16 -0
  53. data/lib/search_object/plugin/paging.rb +42 -0
  54. data/lib/search_object/plugin/sorting.rb +54 -0
  55. data/lib/search_object/plugin/will_paginate.rb +17 -0
  56. data/lib/search_object/search.rb +26 -0
  57. data/lib/search_object/version.rb +3 -0
  58. data/search_object.gemspec +31 -0
  59. data/spec/search_object/base_spec.rb +237 -0
  60. data/spec/search_object/helper_spec.rb +30 -0
  61. data/spec/search_object/plugin/kaminari_spec.rb +50 -0
  62. data/spec/search_object/plugin/model_spec.rb +22 -0
  63. data/spec/search_object/plugin/paging_spec.rb +43 -0
  64. data/spec/search_object/plugin/sorting_spec.rb +139 -0
  65. data/spec/search_object/plugin/will_paginate_spec.rb +51 -0
  66. data/spec/search_object/search_spec.rb +72 -0
  67. data/spec/spec_helper.rb +13 -0
  68. data/spec/spec_helper_active_record.rb +19 -0
  69. data/spec/support/kaminari_setup.rb +7 -0
  70. metadata +292 -0
@@ -0,0 +1,3 @@
1
+ module SearchObject
2
+ VERSION = '0.1'
3
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'search_object/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "search_object"
8
+ spec.version = SearchObject::VERSION
9
+ spec.authors = ["Radoslav Stankov"]
10
+ spec.email = ["rstankov@gmail.com"]
11
+ spec.description = %q{Search object DSL}
12
+ spec.summary = %q{Provides DSL for creating search objects}
13
+ spec.homepage = "https://github.com/RStankov/SearchObject"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
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 "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency 'rspec', '~> 2.14'
24
+ spec.add_development_dependency 'rspec-mocks', '>= 2.12.3'
25
+ spec.add_development_dependency 'activerecord', '>= 3.0.0'
26
+ spec.add_development_dependency 'sqlite3'
27
+ spec.add_development_dependency 'coveralls'
28
+ spec.add_development_dependency 'active_model_lint-rspec'
29
+ spec.add_development_dependency 'will_paginate'
30
+ spec.add_development_dependency 'kaminari'
31
+ end
@@ -0,0 +1,237 @@
1
+ require 'spec_helper'
2
+
3
+ module SearchObject
4
+ describe Base do
5
+ def new_search(default_scope = [], filters = {}, &block)
6
+ search_class = Class.new do
7
+ include SearchObject.module
8
+
9
+ scope { default_scope }
10
+
11
+ if block.nil?
12
+ option :value do |scope, value|
13
+ scope.find_all { |v| v == value }
14
+ end
15
+ else
16
+ class_eval &block
17
+ end
18
+ end
19
+
20
+ search_class.new filters
21
+ end
22
+
23
+ it "can had its #initialize method overwritten" do
24
+ search = new_search do
25
+ def initialize(filters = {})
26
+ @initialized = true
27
+ super filters
28
+ end
29
+
30
+ def initialized?
31
+ @initialized
32
+ end
33
+ end
34
+
35
+ expect(search).to be_initialized
36
+ end
37
+
38
+ it "can have multiple subclasses" do
39
+ search1 = new_search [1, 2, 3], filter: 1 do
40
+ option :filter do |scope, value|
41
+ scope.select { |v| v == value }
42
+ end
43
+ end
44
+
45
+ search2 = new_search [1, 2, 3], filter: 1 do
46
+ option :filter, 2 do |scope, value|
47
+ scope.reject { |v| v == value }
48
+ end
49
+ end
50
+
51
+ expect(search1.results).not_to eq search2.results
52
+ end
53
+
54
+ context "no scope" do
55
+ def search_class
56
+ Class.new do
57
+ include SearchObject.module
58
+
59
+ option :name do
60
+ end
61
+ end
62
+ end
63
+
64
+ it "treats first argument as scope" do
65
+ expect(search_class.new('scope').results).to eq 'scope'
66
+ end
67
+
68
+ it "treats second argument as filters" do
69
+ expect(search_class.new('scope', name: 'name').params).to eq 'name' => 'name'
70
+ end
71
+ end
72
+
73
+ describe "option" do
74
+ it "has default filter" do
75
+ scope = [1, 2, 3]
76
+ expect(scope).to receive(:where).with('value' => 1) { 'results' }
77
+
78
+ search = new_search scope, value: 1 do
79
+ option :value
80
+ end
81
+
82
+ expect(search.results).to eq 'results'
83
+ end
84
+
85
+ it "returns the scope if nil returned" do
86
+ scope = [1, 2, 3]
87
+ search = new_search scope, value: 'some' do
88
+ option :value do
89
+ nil
90
+ end
91
+ end
92
+
93
+ expect(search.results).to eq scope
94
+ end
95
+
96
+ it "can use methods from the object" do
97
+ search1 = new_search [1, 2, 3], filter: 1 do
98
+ option :filter do |scope, value|
99
+ some_instance_method(scope, value)
100
+ end
101
+
102
+ private
103
+
104
+ def some_instance_method(scope, value)
105
+ scope.select { |v| v == value }
106
+ end
107
+ end
108
+
109
+ expect(search1.results).to eq [1]
110
+ end
111
+ end
112
+
113
+ describe "option attributes" do
114
+ it "access option values" do
115
+ search = new_search [], value: 1
116
+ expect(search.value).to eq 1
117
+ end
118
+
119
+ it "returns default option value if option is not specified" do
120
+ search = new_search do
121
+ option :value, 1
122
+ end
123
+ expect(search.value).to eq 1
124
+ end
125
+
126
+ it "does not include invalid options" do
127
+ search = new_search [], invalid: 'option'
128
+ expect { search.invalid }.to raise_error NoMethodError
129
+ end
130
+ end
131
+
132
+ describe "#results" do
133
+ it "returns only the filtered search results" do
134
+ search = new_search [1 ,2 ,3], value: 1
135
+ expect(search.results).to eq [1]
136
+ end
137
+
138
+ it "can apply several options" do
139
+ scope = [1, 2, 3, 4, 5, 6, 7]
140
+ search = new_search scope, bigger_than: 3, odd: true do
141
+ option :bigger_than do |scope, value|
142
+ scope.find_all { |v| v > value }
143
+ end
144
+
145
+ option :odd do |scope, value|
146
+ scope.find_all(&:odd?) if value
147
+ end
148
+ end
149
+
150
+ expect(search.results).to eq [5, 7]
151
+ end
152
+
153
+ it "ignores invalid filters" do
154
+ search = new_search [1, 2, 3], invalid: 'option'
155
+ expect(search.results).to eq [1, 2, 3]
156
+ end
157
+
158
+ it "can be overwritten by overwriting #fetch_results" do
159
+ search = new_search [1, 2, 3], value: 1 do
160
+ option :value do |scope, value|
161
+ scope.find_all { |v| v == value }
162
+ end
163
+
164
+ def fetch_results
165
+ super.map { |v| "~#{v}~" }
166
+ end
167
+ end
168
+
169
+ expect(search.results).to eq ['~1~']
170
+ end
171
+
172
+ it "applies to default options" do
173
+ search = new_search [1,2,3] do
174
+ option :value, 1 do |scope, value|
175
+ scope.select { |v| v == value }
176
+ end
177
+ end
178
+ expect(search.results).to eq [1]
179
+ end
180
+ end
181
+
182
+ describe "#results?" do
183
+ it "returns true if there are results" do
184
+ expect(new_search([1,2,3], value: 1).results?).to be_true
185
+ end
186
+
187
+ it "returns false if there aren't any results" do
188
+ expect(new_search([1,2,3], value: 4).results?).to be_false
189
+ end
190
+ end
191
+
192
+ describe "#count" do
193
+ it "counts the number of results" do
194
+ expect(new_search([1,2,3], value: 1).count).to eq 1
195
+ end
196
+
197
+ it "can't be bypassed by plug ins" do
198
+ search = new_search [1,2,3] do
199
+ def fetch_results
200
+ []
201
+ end
202
+ end
203
+
204
+ expect(search.count).to eq 3
205
+ end
206
+ end
207
+
208
+ describe "#params" do
209
+ it "exports options as params" do
210
+ search = new_search [], value: 1
211
+ expect(search.params).to eq 'value' => 1
212
+ end
213
+
214
+ it "can overwrite options (mainly used for url handers)" do
215
+ search = new_search [], value: 1
216
+ expect(search.params(value: 2)).to eq 'value' => 2
217
+ end
218
+
219
+ it "ignores missing options" do
220
+ search = new_search
221
+ expect(search.params).to eq({})
222
+ end
223
+
224
+ it "ignores invalid options" do
225
+ search = new_search [], invalid: 'option'
226
+ expect(search.params).to eq({})
227
+ end
228
+
229
+ it "includes default options" do
230
+ search = new_search do
231
+ option :value, 1
232
+ end
233
+ expect(search.params).to eq 'value' => 1
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ module SearchObject
4
+ describe Helper do
5
+ describe ".stringify_keys" do
6
+ it "converts hash keys to strings" do
7
+ hash = Helper.stringify_keys a: 1, b: nil, c: false
8
+ expect(hash).to eq 'a' => 1, 'b' => nil, 'c' => false
9
+ end
10
+ end
11
+
12
+ describe ".select_keys" do
13
+ it "selects only given keys" do
14
+ hash = Helper.select_keys({a: 1, b: 2, c:3}, [:a, :b])
15
+ expect(hash).to eq a: 1, b: 2
16
+ end
17
+
18
+ it "ignores not existing keys" do
19
+ hash = Helper.select_keys({}, [:a, :b])
20
+ expect(hash).to eq({})
21
+ end
22
+ end
23
+
24
+ describe "camelize" do
25
+ it "transforms :paging to 'Paging'" do
26
+ expect(Helper.camelize(:paging)).to eq 'Paging'
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper_active_record'
2
+
3
+ require_relative '../../support/kaminari_setup'
4
+
5
+ module SearchObject
6
+ module Plugin
7
+ describe Paging do
8
+ def search_class
9
+ Class.new do
10
+ include SearchObject.module(:kaminari)
11
+
12
+ scope { Product }
13
+
14
+ per_page 2
15
+ end
16
+ end
17
+
18
+ after do
19
+ Product.delete_all
20
+ end
21
+
22
+ it "paginates" do
23
+ 10.times { |i| Product.create name: "product_#{i}" }
24
+ search = search_class.new({}, 3)
25
+ expect(search.results.map(&:name)).to eq %w(product_4 product_5)
26
+ end
27
+
28
+ it "uses will paginate" do
29
+ search = search_class.new
30
+ expect(search.results.respond_to? :total_pages).to be_true
31
+ end
32
+
33
+ it "treats nil page as 0" do
34
+ search = search_class.new({}, nil)
35
+ expect(search.page).to eq 0
36
+ end
37
+
38
+ it "treats negative page numbers as positive" do
39
+ search = search_class.new({}, -1)
40
+ expect(search.page).to eq 1
41
+ end
42
+
43
+ it "gives the real count" do
44
+ 10.times { |i| Product.create name: "product_#{i}" }
45
+ search = search_class.new({}, 1)
46
+ expect(search.count).to eq 10
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ require 'active_model_lint-rspec'
4
+
5
+ module SearchObject
6
+ module Plugin
7
+ class ExtendedModel
8
+ include SearchObject.module(:model)
9
+
10
+ # Fake errors
11
+ # Since SearchObject is focused to plain search forms,
12
+ # which don't have validation most of the time
13
+ def errors
14
+ Hash.new([])
15
+ end
16
+ end
17
+
18
+ describe ExtendedModel do
19
+ it_behaves_like 'an ActiveModel'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper_active_record'
2
+
3
+ module SearchObject
4
+ module Plugin
5
+ describe Paging do
6
+ def search_class
7
+ Class.new do
8
+ include SearchObject.module(:paging)
9
+
10
+ scope { Product }
11
+
12
+ per_page 2
13
+ end
14
+ end
15
+
16
+ after do
17
+ Product.delete_all
18
+ end
19
+
20
+ it "paginates results (by offset and limit)" do
21
+ 10.times { |i| Product.create name: "product_#{i}" }
22
+ search = search_class.new({}, 1)
23
+ expect(search.results.map(&:name)).to eq %w(product_2 product_3)
24
+ end
25
+
26
+ it "treats nil page as 0" do
27
+ search = search_class.new({}, nil)
28
+ expect(search.page).to eq 0
29
+ end
30
+
31
+ it "treats negative page numbers as positive" do
32
+ search = search_class.new({}, -1)
33
+ expect(search.page).to eq 1
34
+ end
35
+
36
+ it "gives the real count" do
37
+ 10.times { |i| Product.create name: "product_#{i}" }
38
+ search = search_class.new({}, 1)
39
+ expect(search.count).to eq 10
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,139 @@
1
+ require 'spec_helper_active_record'
2
+
3
+ module SearchObject
4
+ module Plugin
5
+ describe Sorting do
6
+ def search_class
7
+ Class.new do
8
+ include SearchObject.module(:sorting)
9
+
10
+ scope { Product.all }
11
+
12
+ sort_by :name, :price
13
+
14
+ option :name
15
+ option :price
16
+ end
17
+ end
18
+
19
+ describe "sorting" do
20
+ after do
21
+ Product.delete_all
22
+ end
23
+
24
+ it "sorts results based on the sort option" do
25
+ 5.times { |i| Product.create! price: i }
26
+
27
+ search = search_class.new sort: 'price desc'
28
+ expect(search.results.map(&:price)).to eq [4, 3, 2, 1, 0]
29
+ end
30
+
31
+ it "defaults to first sort by option" do
32
+ 5.times { |i| Product.create! name: "Name#{i}" }
33
+
34
+ search = search_class.new
35
+ expect(search.results.map(&:name)).to eq %w(Name4 Name3 Name2 Name1 Name0)
36
+ end
37
+
38
+ it "ignores invalid sort values" do
39
+ search = search_class.new sort: 'invalid attribute'
40
+ expect { search.results.to_a }.not_to raise_error
41
+ end
42
+ end
43
+
44
+ describe "#sort?" do
45
+ it "matches the sort option" do
46
+ search = search_class.new sort: 'price desc'
47
+
48
+ expect(search.sort?(:price)).to be_true
49
+ expect(search.sort?(:name)).to be_false
50
+ end
51
+
52
+ it "matches string also" do
53
+ search = search_class.new sort: 'price desc'
54
+
55
+ expect(search.sort?('price')).to be_true
56
+ expect(search.sort?('name')).to be_false
57
+ end
58
+
59
+ it "matches exact strings" do
60
+ search = search_class.new sort: 'price desc'
61
+
62
+ expect(search.sort?('price desc')).to be_true
63
+ expect(search.sort?('price asc')).to be_false
64
+ end
65
+ end
66
+
67
+ describe "#sort_attribute" do
68
+ it "returns sort option attribute" do
69
+ search = search_class.new sort: 'price desc'
70
+ expect(search.sort_attribute).to eq 'price'
71
+ end
72
+
73
+ it "defaults to the first sort by option" do
74
+ search = search_class.new
75
+ expect(search.sort_attribute).to eq 'name'
76
+ end
77
+
78
+ it "rejects invalid sort options, uses defaults" do
79
+ search = search_class.new sort: 'invalid'
80
+ expect(search.sort_attribute).to eq 'name'
81
+ end
82
+ end
83
+
84
+ describe "#sort_direction" do
85
+ it "returns asc or desc" do
86
+ expect(search_class.new(sort: 'price desc').sort_direction).to eq 'desc'
87
+ expect(search_class.new(sort: 'price asc').sort_direction).to eq 'asc'
88
+ end
89
+
90
+ it "defaults to desc" do
91
+ expect(search_class.new.sort_direction).to eq 'desc'
92
+ expect(search_class.new(sort: 'price').sort_direction).to eq 'desc'
93
+ end
94
+
95
+ it "rejects invalid sort options, uses desc" do
96
+ expect(search_class.new(sort: 'price foo').sort_direction).to eq 'desc'
97
+ end
98
+ end
99
+
100
+ describe "#sort_direction_for" do
101
+ it "returns desc if current sort attribute is not the given attribute" do
102
+ expect(search_class.new(sort: 'price desc').sort_direction_for('name')).to eq 'desc'
103
+ end
104
+
105
+ it "returns asc if current sort attribute is the given attribute" do
106
+ expect(search_class.new(sort: 'name desc').sort_direction_for('name')).to eq 'asc'
107
+ end
108
+
109
+ it "returns desc if current sort attribute is the given attribute, but asc with direction" do
110
+ expect(search_class.new(sort: 'name asc').sort_direction_for('name')).to eq 'desc'
111
+ end
112
+ end
113
+
114
+ describe "#sort_params_for" do
115
+ it "adds sort direction" do
116
+ search = search_class.new sort: 'name', name: 'test'
117
+ expect(search.sort_params_for(:price)).to eq 'sort' => 'price desc', 'name' => 'test'
118
+ end
119
+
120
+ it "reverses sort direction if this is the current sort attribute" do
121
+ search = search_class.new sort: 'name desc', name: 'test'
122
+ expect(search.sort_params_for(:name)).to eq 'sort' => 'name asc', 'name' => 'test'
123
+ end
124
+
125
+ it "accepts additional options" do
126
+ search = search_class.new
127
+ expect(search.sort_params_for(:price, name: 'value')).to eq 'sort' => 'price desc', 'name' => 'value'
128
+ end
129
+ end
130
+
131
+ describe "#reverted_sort_direction" do
132
+ it "reverts sorting direction" do
133
+ expect(search_class.new(sort: 'price desc').reverted_sort_direction).to eq 'asc'
134
+ expect(search_class.new(sort: 'price asc').reverted_sort_direction).to eq 'desc'
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end