searchable_record 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,7 @@
1
+ === 0.0.2 / 2008-08-26
2
+
3
+ * Changed specs to use RSpec syntax instead of test/spec.
4
+
5
+ === 0.0.1 / 2008-03-03
6
+
7
+ * First release.
data/MIT-LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2008 Tuomas Kareinen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19
+ IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,10 @@
1
+ History.txt
2
+ Manifest.txt
3
+ MIT-LICENSE.txt
4
+ Rakefile
5
+ README.txt
6
+ lib/searchable_record.rb
7
+ lib/util.rb
8
+ spec/searchable_record_spec_helper.rb
9
+ spec/searchable_record_spec.rb
10
+ spec/util_spec.rb
data/README.txt ADDED
@@ -0,0 +1,128 @@
1
+ = SearchableRecord
2
+
3
+ SearchableRecord is a small Ruby on Rails plugin that makes the parsing of
4
+ query parameters from URLs easy for resources, allowing the requester to
5
+ control the items (records) shown in the resource's representation.
6
+
7
+ The implementation is a helper module (a mixin) for ActiveRecord models. It
8
+ is used by including SearchableRecord module in a model.
9
+
10
+ The mixin provides a class method, <tt>SearchableRecord#find_queried</tt>,
11
+ to the class that includes it. The method is a front-end to
12
+ ActiveRecord::Base#find: it parses query parameters against the given rules
13
+ and calls <tt>find</tt> accordingly, returning the results of <tt>find</tt>.
14
+
15
+ == A usage example
16
+
17
+ The following example, although a bit contrived, allows the client to
18
+
19
+ * limit the number of items as the result of the search
20
+ (<tt>limit</tt> parameter),
21
+ * set an offset for the items (<tt>offset</tt> parameter, intended to be
22
+ used together with <tt>limit</tt>),
23
+ * sort the items either in ascending (<tt>sort</tt> parameter) or
24
+ descending (<tt>rsort</tt> parameter) order by items' type and name,
25
+ * to limit the result by matching only items that were update before
26
+ (<tt>until</tt> parameter) or after (<tt>since</tt> parameter) a certain
27
+ date, and
28
+ * to limit the result by matching only items with certain kind of
29
+ types (<tt>type</tt> parameter) or names (<tt>name</tt> parameter), or
30
+ both (for a name, a conversion to the client supplied parameter must be
31
+ applied before matching the name in the database).
32
+
33
+
34
+ These requirements for the query parameters are expressed as the following
35
+ rules:
36
+
37
+ rules = {
38
+ :limit => nil, # key as a flag; the value for the key is not used
39
+ :offset => nil, # key as a flag
40
+ :sort => { 'name' => 'items.name', 'created' => 'items.created_at' },
41
+ :rsort => nil, # rsort is allowed according to rules in :sort (key as a flag)
42
+ :since => 'items.created_at', # cast parameter value as the default type
43
+ :until => 'items.created_at', # cast parameter value as the default type
44
+ :patterns => { :type => 'items.type', # match the pattern with the default operator and converter
45
+ :name => { :column => 'items.name',
46
+ :converter => lambda { |val| "%#{val.gsub('_', '.')}%" } } }
47
+ # match the pattern with the default operator
48
+ }
49
+
50
+ The client uses the URL
51
+ <tt>http://example-site.org/items?limit=5&offset=4&rsort=name&since=2008-02-28&name=foo_bar</tt>
52
+ to fetch a representation of the resource containing the items. The action
53
+ results to the following parameters:
54
+
55
+ # => query_params = {
56
+ # 'offset' => '4',
57
+ # 'limit' => '5',
58
+ # 'rsort' => 'name',
59
+ # 'until' => '2008-02-28',
60
+ # 'name' => 'foo_bar',
61
+ # ...
62
+ # # plus Rails-specific parameters, such as 'action' and 'controller'
63
+ # }
64
+
65
+ In addition, the application happens to require some options to be passed to
66
+ <tt>find</tt>:
67
+
68
+ options = {
69
+ :include => [ :owners ],
70
+ :conditions => "items.flag = 'f'"
71
+ }
72
+
73
+ When <tt>find_queried</tt> is called, with
74
+
75
+ Item.find_queried(:all, query_params, rules, options)
76
+
77
+ the result is the following call to <tt>find</tt>.
78
+
79
+ Item.find(:all,
80
+ :include => [ :owners ],
81
+ :order => 'items.name desc',
82
+ :offset => 4,
83
+ :limit => 5,
84
+ :conditions => [ "(items.flag = 'f') and (items.created_at <= cast(:until as datetime)) and (items.name like :name)",
85
+ { :until => '2008-02-28', :name => '%foo.bar%' } ])
86
+
87
+ The search result for <tt>find</tt> contains at most 5 items that are
88
+
89
+ * from offset 4 (that is, items from positions 5 to 9),
90
+ * sorted in descending order by items' names,
91
+ * updated since 2008-02-28, and
92
+ * have <tt>foo.bar</tt> in their name.
93
+
94
+ See +find_queried+ method in SearchableRecord::ClassMethods for usage
95
+ documentation.
96
+
97
+ == Installation
98
+
99
+ In order to install the plugin as a Ruby gem for a Rails application,
100
+ edit the <tt>environment.rb</tt> file of the application to contain the
101
+ following line:
102
+
103
+ config.gem "searchable_record"
104
+
105
+ (This requires Rails version 2.1 or above.)
106
+
107
+ Then install the gem, either using the Rakefile of the Rails application:
108
+
109
+ rake gems:install
110
+
111
+ ...or with the <tt>gem</tt> tool:
112
+
113
+ gem install searchable_record
114
+
115
+ Use git to get the source code for modifications and hacks:
116
+
117
+ git clone git://gitorious.org/searchable-rec/mainline.git
118
+
119
+ == Contacting
120
+
121
+ Please send comments, suggestions, bugs, or patches by email to Tuomas
122
+ Kareinen < tkareine (at) gmail (dot) com >.
123
+
124
+ == Legal note
125
+
126
+ Copyright (c) 2008 Tuomas Kareinen.
127
+
128
+ SearchableRecord plugin is licensed under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require 'spec/rake/spectask'
4
+
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + "/lib")
6
+
7
+ require 'searchable_record'
8
+
9
+ Hoe.new('searchable_record', SearchableRecord::Meta::VERSION.to_s) do |p|
10
+ p.name = "searchable_record"
11
+ p.rubyforge_name = 'searchable-rec' # If different than lowercase project name
12
+ p.author = "Tuomas Kareinen"
13
+ p.email = 'tkareine@gmail.com'
14
+ p.summary = "SearchableRecord is a small Ruby on Rails plugin that makes the parsing of
15
+ query parameters from URLs easy for resources, allowing the requester to
16
+ control the items (records) shown in the resource's representation."
17
+ p.description = p.paragraphs_of('README.txt', 1..3).join("\n\n")
18
+ p.url = "http://searchable-rec.rubyforge.org"
19
+ # p.clean_globs = ['test/actual'] # Remove this directory on "rake clean"
20
+ p.remote_rdoc_dir = '' # Release to root
21
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
22
+ p.extra_deps = ['activesupport']
23
+ end
24
+
25
+ desc "Run specs."
26
+ Spec::Rake::SpecTask.new('spec') do |t|
27
+ t.spec_files = FileList['spec/**/*.rb']
28
+ t.spec_opts = ["--format", "specdoc"]
29
+ #t.warning = true
30
+ end
31
+
32
+ desc "Search unfinished parts of source code."
33
+ task :todo do
34
+ FileList['**/*.rb'].egrep /#.*(TODO|FIXME)/
35
+ end
@@ -0,0 +1,267 @@
1
+ unless defined? ActiveSupport
2
+ require 'rubygems'
3
+ gem 'activesupport'
4
+ require 'active_support'
5
+ end
6
+ require 'util'
7
+
8
+ # See SearchableRecord::ClassMethods#find_queried for usage documentation.
9
+ module SearchableRecord
10
+ module Meta #:nodoc:
11
+ module VERSION #:nodoc:
12
+ MAJOR = 0
13
+ MINOR = 0
14
+ BUILD = 2
15
+
16
+ def self.to_s
17
+ [ MAJOR, MINOR, BUILD ].join('.')
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.included(base_class) #:nodoc:
23
+ base_class.class_eval do
24
+ extend ClassMethods
25
+
26
+ @@searchable_record_settings = {
27
+ :cast_since_as => 'datetime',
28
+ :cast_until_as => 'datetime',
29
+ :pattern_operator => 'like',
30
+ :pattern_converter => lambda { |val| "%#{val}%" }
31
+ }
32
+
33
+ cattr_accessor :searchable_record_settings
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ module ClassMethods
40
+ # === Description
41
+ #
42
+ # Parses the query parameters the client has given in the URL of the HTTP
43
+ # request. With the query parameters, the client may set a limit, an
44
+ # offset, or an ordering to the items in the search result. In addition,
45
+ # the client may limit the output by allowing only certain records that
46
+ # match to specific patterns.
47
+ #
48
+ # What the client user is allowed to query is defined by specific rules
49
+ # passed to the method as a Hash argument. Query parameters that are not
50
+ # explicitly stated in the rules are silently discarded.
51
+ #
52
+ # Essentially, the method is a frontend for
53
+ # ActiveRecord::Base#find. The method
54
+ #
55
+ # 1. parses the query parameters the client has given in the URL for the
56
+ # HTTP request (+query_params+) against the rules (+rules+), and
57
+ # 2. calls <tt>find</tt> with the parsed options.
58
+ #
59
+ # ==== Parsing rules
60
+ #
61
+ # The parsing rules must be given as a Hash; the keys in the hash indicate
62
+ # the parameters that are allowed. The recognized keys are the following:
63
+ #
64
+ # * <tt>:limit</tt>, which uses nil as the value (the same effect as with
65
+ # <tt>find</tt>).
66
+ # * <tt>:offset</tt>, which uses nil as the value (the same effect as with
67
+ # <tt>find</tt>).
68
+ # * <tt>:sort</tt>, which determines the ordering. The value is a Hash of
69
+ # <tt>'parameter value' => 'internal table column'</tt> pairs (the same
70
+ # effect as with the <tt>:order</tt> option of
71
+ # <tt>find</tt>);
72
+ # * <tt>:rsort</tt>, for reverse sort. Uses the rules of +:sort+; thus,
73
+ # use <tt>nil</tt> as the value.
74
+ # * <tt>:since</tt>, which sets a lower timedate limit. The value is
75
+ # either a string naming the database table column that has timestamps
76
+ # (using the type from default settings' <tt>:cast_since_as</tt> entry)
77
+ # or a Hash that contains entries <tt>:column => 'table.column'</tt> and
78
+ # <tt>:cast_as => '<sql_timedate_type>'</tt>.
79
+ # * <tt>:until</tt>, which sets an upper timedate limit. Used like
80
+ # <tt>:since</tt>.
81
+ # * <tt>:patterns</tt>, where the value is a Hash containing patterns. The
82
+ # keys in the Hash correspond to additional query parameters and the
83
+ # corresponding values to database table columns. For each pattern,
84
+ # the value is either directly a string, or a Hash containing the
85
+ # entry <tt>:column => 'table.column'</tt>. In addition, the Hash may
86
+ # contain optional entries
87
+ # <tt>:converter => lambda { |val| <conversion_operation_for_val> }</tt>
88
+ # and <tt>:operator => '<sql_pattern_operator>'</tt>.
89
+ # <tt>:converter</tt> expects a block that modifies the input value; if
90
+ # the key is not used, the converter specified in
91
+ # <tt>:pattern_converter</tt> in the default settings is used.
92
+ # <tt>:pattern_operator</tt> specifies a custom match operator for the
93
+ # pattern; if the key is not used, the operator specified in
94
+ # <tt>:pattern_operator</tt> in default settings is used.
95
+ #
96
+ # If both +sort+ and +rsort+ parameters are given in the URL and both are
97
+ # allowed by the rules, +sort+ is favored over +rsort+. Unlike with +sort+
98
+ # and +rsort+ rules (+rsort+ uses the rules of +sort+), the rules for
99
+ # +since+ and +until+ are independent from each other.
100
+ #
101
+ # For usage examples, see the example in README and the unit tests that
102
+ # come with the plugin.
103
+ #
104
+ # ==== Default settings for rules
105
+ #
106
+ # The default settings for the rules are accessible and modifiable by
107
+ # calling the method +searchable_record_settings+. The settings are
108
+ # stored as a Hash; the following keys are recognized:
109
+ #
110
+ # * <tt>:cast_since_as</tt>,
111
+ # * <tt>:cast_until_as</tt>,
112
+ # * <tt>:pattern_operator</tt>, and
113
+ # * <tt>:pattern_converter</tt>.
114
+ #
115
+ # See the parsing rules above how the default settings are used.
116
+ #
117
+ # === Arguments
118
+ #
119
+ # +extend+:: The same as the first argument to <tt>find</tt> (such as <tt>:all</tt>).
120
+ # +query_params+:: The (unsafe) query parameters from the URL.
121
+ # +rules+:: The parsing rules as a Hash.
122
+ # +options+:: Additional options for <tt>find</tt>, such as <tt>:include => [ :an_association ]</tt>.
123
+ #
124
+ # === Return
125
+ #
126
+ # The same as with ActiveRecord::Base#find.
127
+ def find_queried(extend, query_params, rules, options = { })
128
+ query_params = preserve_allowed_query_params(query_params, rules)
129
+
130
+ unless query_params.empty?
131
+ parse_offset(options, query_params)
132
+ parse_limit(options, query_params)
133
+ parse_order(options, query_params, rules)
134
+ parse_conditions(options, query_params, rules)
135
+ end
136
+
137
+ logger.debug("find_queried: query_params=<<#{query_params.inspect}>>, resulted options=<<#{options.inspect}>>")
138
+
139
+ self.find(extend, options)
140
+ end
141
+
142
+ private
143
+
144
+ def preserve_allowed_query_params(query_params, rules)
145
+ allowed_keys = rules.keys
146
+
147
+ # Add pattern matching parameters to the list of allowed keys.
148
+ allowed_keys.delete(:patterns)
149
+ if rules[:patterns]
150
+ allowed_keys += rules[:patterns].keys
151
+ end
152
+
153
+ # Do not affect the passed query parameters.
154
+ Util.pruned_dup(query_params, allowed_keys)
155
+ end
156
+
157
+ def parse_offset(options, query_params)
158
+ if query_params[:offset]
159
+ value = Util.parse_positive_int(query_params[:offset])
160
+ options[:offset] = value unless value.nil?
161
+ end
162
+ end
163
+
164
+ def parse_limit(options, query_params)
165
+ if query_params[:limit]
166
+ value = Util.parse_positive_int(query_params[:limit])
167
+ options[:limit] = value unless value.nil?
168
+ end
169
+ end
170
+
171
+ def parse_order(options, query_params, rules)
172
+ # Sort is favored over rsort.
173
+
174
+ if query_params[:rsort]
175
+ raise ArgumentError, "No sort rule specified." if rules[:sort].nil?
176
+
177
+ sort_by = rules[:sort][query_params[:rsort]]
178
+ options[:order] = sort_by + ' desc' unless sort_by.nil?
179
+ end
180
+
181
+ if query_params[:sort]
182
+ sort_by = rules[:sort][query_params[:sort]]
183
+ options[:order] = sort_by unless sort_by.nil?
184
+ end
185
+ end
186
+
187
+ def parse_conditions(options, query_params, rules)
188
+ cond_strs = [ ]
189
+ cond_syms = { }
190
+
191
+ # The hash query_params is not empty, therefore, it contains at least
192
+ # some of the allowed query parameters (as Symbols) below. Those
193
+ # parameters that are not identified are ignored silently.
194
+
195
+ parse_since_until(cond_strs, cond_syms, query_params, rules)
196
+ parse_patterns(cond_strs, cond_syms, query_params, rules)
197
+
198
+ construct_conditions(options, cond_strs, cond_syms) unless cond_strs.empty?
199
+ end
200
+
201
+ def parse_since_until(cond_strs, cond_syms, query_params, rules)
202
+ if query_params[:since]
203
+ parse_datetime(cond_strs, cond_syms, query_params, rules, :since)
204
+ end
205
+
206
+ if query_params[:until]
207
+ parse_datetime(cond_strs, cond_syms, query_params, rules, :until)
208
+ end
209
+ end
210
+
211
+ def parse_patterns(cond_strs, cond_syms, query_params, rules)
212
+ if rules[:patterns]
213
+ rules[:patterns].each do |param, rule|
214
+ if query_params[param]
215
+ match_op = searchable_record_settings[:pattern_operator]
216
+ conversion_blk = searchable_record_settings[:pattern_converter]
217
+
218
+ if rule.respond_to?(:to_hash)
219
+ column = rule[:column]
220
+
221
+ # Use custom pattern match operator.
222
+ match_op = rule[:operator] unless rule[:operator].nil?
223
+
224
+ # Use custom converter.
225
+ conversion_blk = rule[:converter] unless rule[:converter].nil?
226
+ else
227
+ column = rule
228
+ end
229
+
230
+ cond_strs << "(#{column} #{match_op} :#{param})"
231
+ cond_syms[param] = conversion_blk.call(query_params[param])
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ def parse_datetime(cond_strs, cond_syms, query_params, rules, type)
238
+ rule = rules[type]
239
+ cast_type = searchable_record_settings["cast_#{type}_as".to_sym]
240
+
241
+ if rule.respond_to?(:to_hash)
242
+ column = rule[:column]
243
+
244
+ # Use custom cast type.
245
+ cast_type = rule[:cast_as] unless rule[:cast_as].nil?
246
+ else
247
+ column = rule
248
+ end
249
+
250
+ case type
251
+ when :since then op = '>='
252
+ when :until then op = '<='
253
+ else raise ArgumentError, "Could not determine comparison operator for datetime."
254
+ end
255
+
256
+ cond_strs << "(#{column} #{op} cast(:#{type} as #{cast_type}))"
257
+ cond_syms[type] = query_params[type]
258
+ end
259
+
260
+ def construct_conditions(options, cond_strs, cond_syms)
261
+ conditions = [ cond_strs.join(' and '), cond_syms ]
262
+ preconditions = options[:conditions]
263
+ conditions[0].insert(0, "(#{preconditions}) and ") unless preconditions.nil?
264
+ options[:conditions] = conditions
265
+ end
266
+ end
267
+ end
data/lib/util.rb ADDED
@@ -0,0 +1,28 @@
1
+ module SearchableRecord
2
+ module Util #:nodoc:
3
+ def self.pruned_dup(hash, preserved_keys)
4
+ hash = hash.to_hash
5
+
6
+ dup_hash = { }
7
+
8
+ preserved_keys.to_a.each do |key|
9
+ if !hash[key.to_s].blank? # try to find first with 'key'; if that fails
10
+ dup_hash[key] = hash[key.to_s]
11
+ elsif !hash[key].blank? # ...then with :key
12
+ dup_hash[key] = hash[key]
13
+ end
14
+ end
15
+
16
+ return dup_hash
17
+ end
18
+
19
+ def self.parse_positive_int(str)
20
+ integer = str.to_i
21
+ if integer > 0 # nil.to_i == 0
22
+ return integer
23
+ else
24
+ return nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,301 @@
1
+ require File.join(File.dirname(__FILE__) + '/searchable_record_spec_helper')
2
+ require 'logger'
3
+ require 'searchable_record'
4
+
5
+ class Record; include SearchableRecord; end
6
+
7
+ describe SearchableRecord, "for finding queried records" do
8
+ before(:each) do
9
+ Record.stubs(:logger).returns(stub(:debug))
10
+ #Record.stubs(:logger).returns(Logger.new(STDOUT))
11
+ end
12
+
13
+ it "should be able to modify default settings" do
14
+ new_settings = {
15
+ :cast_since_as => 'time',
16
+ :cast_until_as => 'date',
17
+ :pattern_operator => 'regexp',
18
+ :pattern_converter => lambda { |val| val }
19
+ }
20
+
21
+ org_settings = Record.searchable_record_settings.dup
22
+
23
+ Record.searchable_record_settings = new_settings
24
+ Record.searchable_record_settings.should == new_settings
25
+
26
+ Record.searchable_record_settings = org_settings
27
+ Record.searchable_record_settings.should == org_settings
28
+ end
29
+
30
+ it "should execute find with no parameters, discarding unrecognized parameters" do
31
+ Record.expects(:find).times(2).with(:all, { })
32
+
33
+ Record.find_queried(:all, { }, { })
34
+ Record.find_queried(:all, { :foo => 'bar' }, { })
35
+ end
36
+
37
+ it "should execute find with a positive offset parameter" do
38
+ Record.expects(:find).times(1).with(:all, { :offset => 1 })
39
+
40
+ Record.find_queried(:all, { :offset => '1' }, { :offset => nil })
41
+
42
+ Record.expects(:find).times(2).with(:all, { :offset => 4 })
43
+
44
+ Record.find_queried(:all, { :offset => '4' }, { :offset => nil })
45
+ Record.find_queried(:all, { :offset => '4', :limit => '3' }, { :offset => nil })
46
+ end
47
+
48
+ it "should discard other than positive offset parameters" do
49
+ Record.expects(:find).times(4).with(:all, { })
50
+
51
+ Record.find_queried(:all, { }, { :offset => nil })
52
+ Record.find_queried(:all, { :offset => 'aii' }, { :offset => nil })
53
+ Record.find_queried(:all, { :offset => '0' }, { :offset => nil })
54
+ Record.find_queried(:all, { :offset => '-1' }, { :offset => nil })
55
+ end
56
+
57
+ it "should execute find with a positive limit parameter" do
58
+ Record.expects(:find).times(1).with(:all, { :limit => 1 })
59
+
60
+ Record.find_queried(:all, { :limit => '1' }, { :limit => nil })
61
+
62
+ Record.expects(:find).times(2).with(:all, { :limit => 4 })
63
+
64
+ Record.find_queried(:all, { :limit => '4' }, { :limit => nil })
65
+ Record.find_queried(:all, { :limit => '4', :offset => '3' }, { :limit => nil })
66
+ end
67
+
68
+ it "should discard other than positive limit parameters" do
69
+ Record.expects(:find).times(4).with(:all, { })
70
+
71
+ Record.find_queried(:all, { }, { :limit => nil })
72
+ Record.find_queried(:all, { :limit => 'aii' }, { :limit => nil })
73
+ Record.find_queried(:all, { :limit => '0' }, { :limit => nil })
74
+ Record.find_queried(:all, { :limit => '-1' }, { :limit => nil })
75
+ end
76
+
77
+ it "should execute find with sort parameter" do
78
+ Record.expects(:find).times(1).with(:all, { :order => 'users.first_name' })
79
+
80
+ Record.find_queried(:all, { :sort => 'first_name' },
81
+ { :sort => { 'first_name' => 'users.first_name' } })
82
+ end
83
+
84
+ it "should execute find with reverse sort parameter" do
85
+ Record.expects(:find).times(1).with(:all, { :order => 'users.first_name desc' })
86
+
87
+ Record.find_queried(:all, { :rsort => 'first_name' },
88
+ { :sort => { 'first_name' => 'users.first_name' },
89
+ :rsort => nil })
90
+ end
91
+
92
+ it "should raise an exception if rsort is specified without sort rule" do
93
+ lambda { Record.find_queried(:all, { :rsort => 'first_name' },
94
+ { :rsort => { 'first_name' => 'users.first_name' } }) }.should raise_error(ArgumentError)
95
+ end
96
+
97
+ it "should execute find with sort parameter, favoring 'sort' over 'rsort'" do
98
+ Record.expects(:find).times(1).with(:all, { :order => 'users.first_name' })
99
+
100
+ Record.find_queried(:all, { :sort => 'first_name', :rsort => 'last_name' },
101
+ { :sort => { 'first_name' => 'users.first_name',
102
+ 'last_name' => 'users.last_name' },
103
+ :rsort => nil })
104
+ end
105
+
106
+ it "should discard other than specified sort parameters" do
107
+ Record.expects(:find).times(1).with(:all, { })
108
+
109
+ Record.find_queried(:all, { :sort => 'first' },
110
+ { :sort => { 'first_name' => 'users.first_name' } })
111
+
112
+ Record.expects(:find).times(1).with(:all, { :order => 'users.last_name desc' })
113
+
114
+ Record.find_queried(:all, { :sort => 'last', :rsort => 'last_name' },
115
+ { :sort => { 'first_name' => 'users.first_name',
116
+ 'last_name' => 'users.last_name' },
117
+ :rsort => nil })
118
+ end
119
+
120
+ it "should execute find with since parameter, with default settings" do
121
+ Record.expects(:find).times(2).with(:all, { :conditions => [ '(users.created_at >= cast(:since as datetime))',
122
+ { :since => '2008-02-26' } ] })
123
+
124
+ Record.find_queried(:all, { :since => '2008-02-26' },
125
+ { :since => 'users.created_at' })
126
+ Record.find_queried(:all, { :since => '2008-02-26' },
127
+ { :since => { :column => 'users.created_at' } })
128
+ end
129
+
130
+ it "should execute find with since parameter, with custom settings" do
131
+ Record.expects(:find).times(3).with(:all, { :conditions => [ '(users.time >= cast(:since as time))',
132
+ { :since => '11:04' } ] })
133
+
134
+ Record.find_queried(:all, { :since => '11:04' },
135
+ { :since => { :column => 'users.time',
136
+ :cast_as => 'time' } })
137
+
138
+ org_settings = Record.searchable_record_settings.dup
139
+ Record.searchable_record_settings[:cast_since_as] = 'time'
140
+
141
+ Record.find_queried(:all, { :since => '11:04' },
142
+ { :since => 'users.time'})
143
+
144
+ Record.find_queried(:all, { :since => '11:04' },
145
+ { :since => { :column => 'users.time' } })
146
+
147
+ Record.searchable_record_settings = org_settings
148
+ end
149
+
150
+ it "should execute find with until parameter, with default settings" do
151
+ Record.expects(:find).times(2).with(:all, { :conditions => [ '(users.created_at <= cast(:until as datetime))',
152
+ { :until => '2008-04-28' } ] })
153
+
154
+ Record.find_queried(:all, { :until => '2008-04-28' },
155
+ { :until => 'users.created_at' })
156
+ Record.find_queried(:all, { :until => '2008-04-28' },
157
+ { :until => { :column => 'users.created_at' } })
158
+ end
159
+
160
+ it "should execute find with until parameter, with custom settings" do
161
+ Record.expects(:find).times(3).with(:all, { :conditions => [ '(users.time <= cast(:until as time))',
162
+ { :until => '13:06' } ] })
163
+
164
+ Record.find_queried(:all, { :until => '13:06' },
165
+ { :until => { :column => 'users.time',
166
+ :cast_as => 'time' } })
167
+
168
+ org_settings = Record.searchable_record_settings.dup
169
+ Record.searchable_record_settings[:cast_until_as] = 'time'
170
+
171
+ Record.find_queried(:all, { :until => '13:06' },
172
+ { :until => 'users.time'})
173
+
174
+ Record.find_queried(:all, { :until => '13:06' },
175
+ { :until => { :column => 'users.time' } })
176
+
177
+ Record.searchable_record_settings = org_settings
178
+ end
179
+
180
+ it "should execute find with pattern matching parameters" do
181
+ Record.expects(:find).times(1).with(:all, :conditions => [ "(users.first_name like :name)",
182
+ { :name => '%john%' } ])
183
+
184
+ Record.find_queried(:all, { :name => 'john' },
185
+ { :patterns => { :name => 'users.first_name'} })
186
+ end
187
+
188
+ it "should execute find with a pattern matching parameter, with default settings" do
189
+ Record.expects(:find).times(1).with(:all, :conditions => [ "(sites.status like :status)",
190
+ { :status => '%active%' } ])
191
+
192
+ Record.find_queried(:all, { :status => 'active' },
193
+ { :patterns => { :status => 'sites.status' } })
194
+ end
195
+
196
+ it "should execute find with a pattern matching parameter, with custom settings" do
197
+ Record.expects(:find).times(1).with(:all, :conditions => [ "(sites.domain like :domain)",
198
+ { :domain => '%www.example.fi%' } ])
199
+
200
+ Record.find_queried(:all, { :domain => 'www_example_fi' },
201
+ { :patterns => { :domain => { :column => 'sites.domain',
202
+ :converter => lambda { |val| "%#{val.gsub('_', '.')}%" } } } })
203
+
204
+ Record.expects(:find).times(2).with(:all, :conditions => [ "(sites.domain regexp :domain)",
205
+ { :domain => 'www.example.fi' } ])
206
+
207
+ Record.find_queried(:all, { :domain => 'www_example_fi' },
208
+ { :patterns => { :domain => { :column => 'sites.domain',
209
+ :converter => lambda { |val| val.gsub('_', '.') },
210
+ :operator => 'regexp' } } })
211
+
212
+ org_settings = Record.searchable_record_settings.dup
213
+ Record.searchable_record_settings[:pattern_operator] = 'regexp'
214
+ Record.searchable_record_settings[:pattern_converter] = lambda { |val| val.gsub('_', '.') }
215
+
216
+ Record.find_queried(:all, { :domain => 'www_example_fi' },
217
+ { :patterns => { :domain => 'sites.domain' } })
218
+
219
+ Record.searchable_record_settings = org_settings
220
+ end
221
+
222
+ it "should execute find with multiple pattern matching parameters" do
223
+ results = [
224
+ { :conditions => [ "(sites.domain like :domain) and (sites.status like :status)",
225
+ { :domain => '%www.another.example.fi%',
226
+ :status => '%active%' } ]},
227
+ { :conditions => [ "(sites.status like :status) and (sites.domain like :domain)",
228
+ { :domain => '%www.another.example.fi%',
229
+ :status => '%active%' } ]}
230
+ ]
231
+
232
+ Record.expects(:find).times(1).with(:all, any_of(equals(results[0]), equals(results[1])))
233
+
234
+ Record.find_queried(:all, { :domain => 'www_another_example_fi',
235
+ :status => 'active' },
236
+ { :patterns => { :domain => { :column => 'sites.domain',
237
+ :converter => lambda { |val| "%#{val.gsub('_', '.')}%" } },
238
+ :status => 'sites.status' } })
239
+ end
240
+
241
+ it "should preserve additional options" do
242
+ Record.expects(:find).times(1).with(:all, :include => [ :affiliates ],
243
+ :conditions => [ "(sites.flags = 'fo') and (sites.domain like :domain)",
244
+ { :domain => '%www.still-works.com%' } ])
245
+
246
+ Record.find_queried(:all, { :domain => 'www_still-works_com' },
247
+ { :patterns => { :domain => { :column => 'sites.domain',
248
+ :converter => lambda { |val| "%#{val.gsub('_', '.')}%" } } } },
249
+ { :conditions => "sites.flags = 'fo'",
250
+ :include => [ :affiliates ] })
251
+
252
+ Record.expects(:find).times(1).with(:all, :include => [ :affiliates ],
253
+ :conditions => [ "(sites.flags = 'fo') and (users.time <= cast(:until as time))",
254
+ { :until => '13:06' } ])
255
+
256
+ Record.find_queried(:all, { :until => '13:06' },
257
+ { :until => { :column => 'users.time',
258
+ :cast_as => 'time' } },
259
+ { :conditions => "sites.flags = 'fo'",
260
+ :include => [ :affiliates ] })
261
+ end
262
+
263
+ it "should work with all rules combined" do
264
+ Item = Record
265
+
266
+ Item.expects(:find).times(1).with(:all,
267
+ :include => [ :owners ],
268
+ :order => 'items.name desc',
269
+ :offset => 4,
270
+ :limit => 5,
271
+ :conditions => [ "(items.flag = 'f') and (items.created_at <= cast(:until as datetime)) and (items.name like :name)",
272
+ { :until => '2008-02-28', :name => '%foo.bar%' } ])
273
+
274
+ query_params = {
275
+ :offset => '4',
276
+ :limit => '5',
277
+ :rsort => 'name',
278
+ :until => '2008-02-28',
279
+ :name => 'foo_bar'
280
+ }
281
+
282
+ rules = {
283
+ :limit => nil,
284
+ :offset => nil,
285
+ :sort => { 'name' => 'items.name', 'created' => 'items.created_at' },
286
+ :rsort => nil,
287
+ :since => 'items.created_at',
288
+ :until => 'items.created_at',
289
+ :patterns => { :type => 'items.type',
290
+ :name => { :column => 'items.name',
291
+ :converter => lambda { |val| "%#{val.gsub('_', '.')}%" } } }
292
+ }
293
+
294
+ options = {
295
+ :include => [ :owners ],
296
+ :conditions => "items.flag = 'f'"
297
+ }
298
+
299
+ Item.find_queried(:all, query_params, rules, options)
300
+ end
301
+ end
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'rubygems'
4
+ require 'mocha'
5
+
6
+ Spec::Runner.configure do |config|
7
+ config.mock_with :mocha
8
+ end
data/spec/util_spec.rb ADDED
@@ -0,0 +1,32 @@
1
+ require File.join(File.dirname(__FILE__) + '/searchable_record_spec_helper')
2
+ require 'active_support/core_ext/blank'
3
+ require 'util'
4
+
5
+ include SearchableRecord
6
+
7
+ describe Util do
8
+ it "should parse positive integers" do
9
+ Util.parse_positive_int('1').should == 1
10
+ Util.parse_positive_int('1sdfgsdf').should == 1
11
+ Util.parse_positive_int(nil).should be_nil
12
+ Util.parse_positive_int('0').should be_nil
13
+ Util.parse_positive_int('-1').should be_nil
14
+ Util.parse_positive_int('sdfgdfg').should be_nil
15
+ end
16
+
17
+ it "should prune and duplicate hashes" do
18
+ str = "don't remove me"
19
+
20
+ Util.pruned_dup({ :excess => 'foobar', :preserve => str }, [ :preserve ]).should == { :preserve => str }
21
+ Util.pruned_dup({ :excess => 'foobar', 'preserve' => str }, [ :preserve ]).should == { :preserve => str }
22
+
23
+ # A contrived example of calling #to_a for the second argument.
24
+ Util.pruned_dup({:e => 'foobar', [ :p, str ] => str }, { :p => str }).should == { [:p, str ] => str }
25
+
26
+ Util.pruned_dup({ :excess => 'foobar' }, [ :preserve ]).should == { }
27
+ Util.pruned_dup({ }, [ ]).should == { }
28
+ Util.pruned_dup({ }, [ :preserve ]).should == { }
29
+
30
+ lambda { Util.pruned_dup(nil, [ :foo ]) }.should raise_error(NoMethodError)
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: searchable_record
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Tuomas Kareinen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-27 00:00:00 +03:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: hoe
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.7.0
34
+ version:
35
+ description: "SearchableRecord is a small Ruby on Rails plugin that makes the parsing of query parameters from URLs easy for resources, allowing the requester to control the items (records) shown in the resource's representation. The implementation is a helper module (a mixin) for ActiveRecord models. It is used by including SearchableRecord module in a model. The mixin provides a class method, <tt>SearchableRecord#find_queried</tt>, to the class that includes it. The method is a front-end to ActiveRecord::Base#find: it parses query parameters against the given rules and calls <tt>find</tt> accordingly, returning the results of <tt>find</tt>."
36
+ email: tkareine@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - History.txt
43
+ - Manifest.txt
44
+ - MIT-LICENSE.txt
45
+ - README.txt
46
+ files:
47
+ - History.txt
48
+ - Manifest.txt
49
+ - MIT-LICENSE.txt
50
+ - Rakefile
51
+ - README.txt
52
+ - lib/searchable_record.rb
53
+ - lib/util.rb
54
+ - spec/searchable_record_spec_helper.rb
55
+ - spec/searchable_record_spec.rb
56
+ - spec/util_spec.rb
57
+ has_rdoc: true
58
+ homepage: http://searchable-rec.rubyforge.org
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --main
62
+ - README.txt
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ requirements: []
78
+
79
+ rubyforge_project: searchable-rec
80
+ rubygems_version: 1.2.0
81
+ signing_key:
82
+ specification_version: 2
83
+ summary: SearchableRecord is a small Ruby on Rails plugin that makes the parsing of query parameters from URLs easy for resources, allowing the requester to control the items (records) shown in the resource's representation.
84
+ test_files: []
85
+