search_object 0.1

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.
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