rhodes-framework 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/.gitignore +2 -0
  2. data/History.txt +37 -0
  3. data/Manifest.txt +66 -0
  4. data/README.rdoc +2 -0
  5. data/Rakefile +50 -0
  6. data/lib/ServeME.rb +7 -0
  7. data/lib/TestServe.rb +9 -0
  8. data/lib/bsearch.rb +120 -0
  9. data/lib/builtinME.rb +626 -0
  10. data/lib/date/format.rb +1339 -0
  11. data/lib/date.rb +1792 -0
  12. data/lib/dateME.rb +24 -0
  13. data/lib/erb.rb +896 -0
  14. data/lib/find.rb +81 -0
  15. data/lib/rational.rb +19 -0
  16. data/lib/rationalME.rb +530 -0
  17. data/lib/rho/render.rb +51 -0
  18. data/lib/rho/rho.rb +255 -0
  19. data/lib/rho/rhoapplication.rb +36 -0
  20. data/lib/rho/rhocontact.rb +110 -0
  21. data/lib/rho/rhocontroller.rb +35 -0
  22. data/lib/rho/rhofsconnector.rb +32 -0
  23. data/lib/rho/rhosupport.rb +146 -0
  24. data/lib/rho/rhoviewhelpers.rb +130 -0
  25. data/lib/rho.rb +1 -0
  26. data/lib/rhodes-framework.rb +2 -0
  27. data/lib/rhodes.rb +9 -0
  28. data/lib/rhoframework.rb +38 -0
  29. data/lib/rhofsconnector.rb +1 -0
  30. data/lib/rhom/rhom.rb +58 -0
  31. data/lib/rhom/rhom_db_adapter.rb +185 -0
  32. data/lib/rhom/rhom_db_adapterME.rb +93 -0
  33. data/lib/rhom/rhom_object.rb +69 -0
  34. data/lib/rhom/rhom_object_factory.rb +309 -0
  35. data/lib/rhom/rhom_source.rb +60 -0
  36. data/lib/rhom.rb +1 -0
  37. data/lib/singleton.rb +137 -0
  38. data/lib/time.rb +489 -0
  39. data/lib/version.rb +8 -0
  40. data/res/sqlite3/constants.rb +49 -0
  41. data/res/sqlite3/database.rb +715 -0
  42. data/res/sqlite3/driver/dl/api.rb +154 -0
  43. data/res/sqlite3/driver/dl/driver.rb +307 -0
  44. data/res/sqlite3/driver/native/driver.rb +257 -0
  45. data/res/sqlite3/errors.rb +68 -0
  46. data/res/sqlite3/pragmas.rb +271 -0
  47. data/res/sqlite3/resultset.rb +176 -0
  48. data/res/sqlite3/sqlite3_api.rb +0 -0
  49. data/res/sqlite3/statement.rb +230 -0
  50. data/res/sqlite3/translator.rb +109 -0
  51. data/res/sqlite3/value.rb +57 -0
  52. data/res/sqlite3/version.rb +14 -0
  53. data/rhodes-framework.gemspec +18 -0
  54. data/rhodes.gemspec +18 -0
  55. data/spec/app_manifest.txt +4 -0
  56. data/spec/configs/account.rb +3 -0
  57. data/spec/configs/case.rb +3 -0
  58. data/spec/configs/employee.rb +3 -0
  59. data/spec/rho_controller_spec.rb +144 -0
  60. data/spec/rho_spec.rb +75 -0
  61. data/spec/rhom_object_factory_spec.rb +372 -0
  62. data/spec/rhom_spec.rb +45 -0
  63. data/spec/spec.opts +1 -0
  64. data/spec/spec_helper.rb +49 -0
  65. data/spec/stubs.rb +39 -0
  66. data/spec/syncdbtest.sqlite +0 -0
  67. metadata +202 -0
data/lib/rho/rho.rb ADDED
@@ -0,0 +1,255 @@
1
+ require 'time'
2
+ require 'rho/render'
3
+ require 'rho/rhoapplication'
4
+ require 'rhom'
5
+ require 'rhofsconnector'
6
+
7
+ module Rho
8
+ class RHO
9
+ APPLICATIONS = {}
10
+
11
+ def initialize(app_manifest_filename=nil)
12
+ puts "Calling RHO.initialize"
13
+ Rhom::RhomDbAdapter::open(Rho::RhoFSConnector::get_db_fullpathname)
14
+ if app_manifest_filename
15
+ process_model_dirs(app_manifest_filename)
16
+ else
17
+ process_model_dirs(Rho::RhoFSConnector::get_app_manifest_filename)
18
+ end
19
+ init_sources
20
+ end
21
+
22
+ # make sure we close the database file
23
+ def self.finalize
24
+ Rhom::RhomDbAdapter::close
25
+ end
26
+
27
+ # Return the directories where we need to load configuration files
28
+ def process_model_dirs(app_manifest_filename=nil)
29
+ File.open(app_manifest_filename).each do |line|
30
+ require File.join(File.dirname(app_manifest_filename), line.chop)
31
+ end
32
+ end
33
+
34
+ # setup the sources table and model attributes for all applications
35
+ def init_sources
36
+ if defined? Rho::RhoConfig::sources
37
+
38
+ # quick and dirty way to get unique array of hashes
39
+ uniq_sources = Rho::RhoConfig::sources.values.inject([]) { |result,h|
40
+ result << h unless result.include?(h); result
41
+ }
42
+
43
+ # generate unique source list in database for sync
44
+ uniq_sources.each do |source|
45
+
46
+ src_id = source['source_id']
47
+ url = source['url']
48
+ if !self.source_initialized?(src_id)
49
+ Rhom::RhomDbAdapter::insert_into_table('sources',
50
+ {"source_id"=>src_id,"source_url"=>url})
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def source_initialized?(source_id)
57
+ Rhom::RhomDbAdapter::select_from_table('sources','*', 'source_id'=>source_id).size > 0 ? true : false
58
+ end
59
+
60
+ def get_app(appname)
61
+ if (APPLICATIONS[appname].nil?)
62
+ require RhoApplication::get_app_path(appname)+'application'
63
+ #APPLICATIONS[appname] = Object.const_get(appname+'Application').new
64
+ APPLICATIONS[appname] = Object.const_get('AppApplication').new
65
+ end
66
+ APPLICATIONS[appname]
67
+ end
68
+
69
+ def get_start_path
70
+ Rho::RhoConfig.start_path
71
+ end
72
+
73
+ def get_options_path
74
+ Rho::RhoConfig.options_path
75
+ end
76
+
77
+ def get_rhobundle_zip_url
78
+ Rho::RhoConfig.rhobundle_zip_url
79
+ end
80
+
81
+ def get_rhobundle_zip_pwd
82
+ Rho::RhoConfig.rhobundle_zip_pwd
83
+ end
84
+
85
+ def serve(req)
86
+ begin
87
+ puts 'inside RHO.serve...'
88
+ res = init_response
89
+ get_app(req['application']).send :serve, req, res
90
+ return send_response(res)
91
+ rescue Exception => e
92
+ return send_error(e)
93
+ end
94
+ end
95
+
96
+ def serve_hash(req)
97
+ begin
98
+ puts 'inside RHO.serve...'
99
+ res = init_response
100
+ get_app(req['application']).send :serve, req, res
101
+ return send_response_hash(res)
102
+ rescue Exception => e
103
+ return send_error(e,500,true)
104
+ end
105
+ end
106
+
107
+ def serve_index(index_name)
108
+ begin
109
+ puts 'inside RHO.serve_index: ' + index_name
110
+ res = init_response
111
+ res['request-body'] = RhoController::renderfile(index_name)
112
+ return send_response(res)
113
+ rescue Exception => e
114
+ return send_error(e)
115
+ end
116
+ end
117
+
118
+ def serve_index_hash(index_name)
119
+ begin
120
+ puts 'inside RHO.serve_index: ' + index_name
121
+ res = init_response
122
+ res['request-body'] = RhoController::renderfile(index_name)
123
+ return send_response_hash(res)
124
+ rescue Exception => e
125
+ return send_error(e.message, 500, true)
126
+ end
127
+ end
128
+
129
+ def init_response(status=200,message="OK",body="")
130
+ res = Hash.new
131
+ res['status'] = status
132
+ res['message'] = message
133
+ res['headers'] =
134
+ {
135
+ 'Date' => Time.now.httpdate,
136
+ 'Content-Type' => 'text/html',
137
+ 'Content-Length' => 0,
138
+ 'Connection' => 'close'
139
+ }
140
+ res['request-body'] = body
141
+ res
142
+ end
143
+
144
+ CR = "\x0d"
145
+ LF = "\x0a"
146
+ CRLF = "\x0d\x0a"
147
+
148
+ def send_response(res)
149
+ res['headers']['Content-Length'] = res['request-body'].nil? ? 0 : res['request-body'].length
150
+ data = "HTTP/1.1 #{res['status'].to_s} #{res['message']}" + CRLF
151
+ res['headers'].each{|key, value|
152
+ tmp = key.gsub(/\bwww|^te$|\b\w/){|s| s.upcase }
153
+ data << "#{tmp}: #{value}" << CRLF
154
+ }
155
+ data << CRLF
156
+ data << res['request-body']
157
+ data
158
+ end
159
+
160
+ def send_response_hash(res)
161
+ resp = Hash.new
162
+ res['headers']['Content-Length'] = res['request-body'].nil? ? 0 : res['request-body'].length
163
+ res['headers'].each{|key, value|
164
+ tmp = key.gsub(/\bwww|^te$|\b\w/){|s| s.upcase }
165
+ resp[tmp] = value
166
+ }
167
+ resp['request-body'] = res['request-body']
168
+ resp['status'] = res['status']
169
+ resp['message'] = res['message']
170
+
171
+ resp
172
+ end
173
+
174
+ def send_error(exception=nil,status=500,hash=false)
175
+ body=''
176
+ body << <<-_HTML_STRING_
177
+ <html>
178
+ <head>
179
+ <title>Server Error</title>
180
+ <meta name="viewport" content="width=320"/>
181
+ </head>
182
+ <body>
183
+ <p>
184
+ _HTML_STRING_
185
+ body << 'Error: ' << exception.message << "<br/>" if exception
186
+ body << 'Trace: ' << exception.backtrace.join("\n") if exception
187
+ body << <<-_HTML_STRING_
188
+ </p>
189
+ </body>
190
+ </html>
191
+
192
+ _HTML_STRING_
193
+ if ( hash )
194
+ send_response_hash(init_response(status,"Server error",body))
195
+ else
196
+ send_response(init_response(status,"Server error",body))
197
+ end
198
+ end
199
+ end # RHO
200
+
201
+ # Generic configuration class which accepts hashes with unique keys
202
+ class RhoConfig
203
+ @@sources = {}
204
+ @@start_path = '/'
205
+ @@options_path = '/'
206
+ @@rhobundle_zip_url = nil
207
+ @@rhobundle_zip_pwd = nil
208
+
209
+ class << self
210
+ def sources
211
+ @@sources
212
+ end
213
+
214
+ def options_path
215
+ @@options_path
216
+ end
217
+
218
+ def options_path=(path=nil)
219
+ @@options_path = path if path
220
+ end
221
+
222
+ def start_path
223
+ @@start_path
224
+ end
225
+
226
+ def start_path=(path=nil)
227
+ @@start_path = path if path
228
+ end
229
+
230
+ def rhobundle_zip_url
231
+ @@rhobundle_zip_url
232
+ end
233
+
234
+ def rhobundle_zip_url=(url=nil)
235
+ @@rhobundle_zip_url = url
236
+ end
237
+
238
+ def rhobundle_zip_pwd
239
+ @@rhobundle_zip_pwd
240
+ end
241
+
242
+ def rhobundle_zip_pwd=(pwd=nil)
243
+ @@rhobundle_zip_pwd = pwd
244
+ end
245
+
246
+ def add_source(modelname, new_source=nil)
247
+ if new_source
248
+ unless @@sources[new_source]
249
+ @@sources[modelname] = new_source
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end # RhoConfig
255
+ end # Rho
@@ -0,0 +1,36 @@
1
+ require 'rhom'
2
+ require 'rhofsconnector'
3
+
4
+ module Rho
5
+ class RhoApplication
6
+
7
+ def initialize
8
+ if @rhom.nil?
9
+ @rhom = Rhom::Rhom.new
10
+ end
11
+ end
12
+
13
+ class << self
14
+
15
+ def get_app_path(appname)
16
+ Rho::RhoFSConnector::get_app_path(appname)
17
+ end
18
+
19
+ def get_base_app_path
20
+ Rho::RhoFSConnector::get_base_app_path
21
+ end
22
+
23
+ def get_model_path(appname, modelname)
24
+ Rho::RhoFSConnector::get_model_path(appname, modelname)
25
+ end
26
+
27
+ end
28
+
29
+ def serve(req,res)
30
+ req[:modelpath] = self.class.get_model_path req['application'], req['model']
31
+ require req[:modelpath]+'controller'
32
+ res['request-body'] = (Object.const_get(req['model']+'Controller').new).send :serve, @rhom, req, res
33
+ end
34
+
35
+ end # RhoApplication
36
+ end # Rho
@@ -0,0 +1,110 @@
1
+ require 'bsearch'
2
+
3
+ module Rho
4
+ class RhoContact
5
+ class << self
6
+ def find(param)
7
+ pb = Phonebook::openPhonebook
8
+ if pb.nil?
9
+ puts "Can't open phonebook"
10
+ return nil
11
+ elsif param == :all or param == 'all'
12
+ records = Phonebook::getallPhonebookRecords(pb)
13
+ Phonebook::closePhonebook(pb)
14
+ return records
15
+ else
16
+ record = Phonebook::getPhonebookRecord(pb,param)
17
+ Phonebook::closePhonebook(pb)
18
+ return record
19
+ end
20
+ end
21
+
22
+ def create!(properties)
23
+ pb = Phonebook::openPhonebook
24
+ unless pb.nil?
25
+ record = Phonebook::createRecord(pb)
26
+ if record.nil?
27
+ puts "Can't find record " + properties['id']
28
+ else
29
+ properties.each do |key,value|
30
+ Phonebook::setRecordValue(record,key,value)
31
+ end
32
+ Phonebook::addRecord(pb,record)
33
+ end
34
+ Phonebook::closePhonebook(pb)
35
+ end
36
+ end
37
+
38
+ def destroy(recordId)
39
+ pb = Phonebook::openPhonebook
40
+ unless pb.nil?
41
+ record = Phonebook::openPhonebookRecord(pb,recordId)
42
+ if record.nil?
43
+ puts "Can't find record " + recordId
44
+ else
45
+ Phonebook::deleteRecord(pb,record)
46
+ end
47
+ Phonebook::closePhonebook(pb)
48
+ end
49
+ end
50
+
51
+ def update_attributes(properties)
52
+ pb = Phonebook::openPhonebook
53
+ unless pb.nil?
54
+ record = Phonebook::openPhonebookRecord(pb,properties['id'])
55
+ if record.nil?
56
+ puts "Can't find record " + properties['id']
57
+ else
58
+ properties.each do |key,value|
59
+ Phonebook::setRecordValue(record,key,value)
60
+ end
61
+ Phonebook::saveRecord(pb,record)
62
+ end
63
+ Phonebook::closePhonebook(pb)
64
+ end
65
+ end
66
+
67
+ # Examples of how to use select method:
68
+ #
69
+ # selected = Rho::RhoContact.select('first_name' => 'David') { |x| x[1]['last_name']=='Taylor' }
70
+ # ==> returns record(s) of the David Taylor
71
+ #
72
+ # selected = Rho::RhoContact.select('first_name' => 'Kate')
73
+ # ==> Returns all records of Kate
74
+ #
75
+ # selected = Rho::RhoContact.select('last_name' => 'User') do |x|
76
+ # x[1]['first_name']=='Test' and x[1]['company_name']=="rhomobile"
77
+ # end
78
+ # ==> returns all records of the Test User from the company rhomobile
79
+ #
80
+ def select(index, &block)
81
+ key, value = index.keys[0], index.values[0]
82
+ if @contacts.nil? or @key != key
83
+ @key, @contacts = key, find(:all).to_a.sort! {|x,y| x[1][key] <=> y[1][key] }
84
+ end
85
+ found = @contacts[@contacts.bsearch_range {|x| x[1][key] <=> value}]
86
+ unless found.nil? or block.nil?
87
+ return found.select(&block)
88
+ end
89
+ return found
90
+ end
91
+
92
+ def select_by_name(first_last_name, &block)
93
+ if @contacts.nil?
94
+ @contacts = find(:all).to_a.sort! do |x,y|
95
+ x[1]['first_name'] + " " + x[1]['last_name'] <=> y[1]['first_name'] + " " + y[1]['last_name']
96
+ end
97
+ end
98
+ range = @contacts.bsearch_range do |x|
99
+ x[1]['first_name'] + " " + x[1]['last_name'] <=> first_last_name
100
+ end
101
+ found = @contacts[range]
102
+ unless found.nil? or block.nil?
103
+ return found.select(&block)
104
+ end
105
+ return found
106
+ end
107
+
108
+ end #<< self
109
+ end # class RhoContact
110
+ end # module Rho
@@ -0,0 +1,35 @@
1
+ require 'rho/render'
2
+ require 'rho/rhosupport'
3
+ require 'rho/rhoviewhelpers'
4
+
5
+ module Rho
6
+ class RhoController
7
+
8
+ def default_action
9
+ return Hash['GET','show','PUT','update','POST','update',
10
+ 'DELETE','delete'][@request['request-method']] unless @request['id'].nil?
11
+ return Hash['GET','index','POST','create'][@request['request-method']]
12
+ end
13
+
14
+ def serve(object_mapping,req,res)
15
+ @request, @response = req, res;
16
+ @object_mapping = object_mapping
17
+ @params = RhoSupport::query_params req
18
+ send req['action'].nil? ? default_action : req['action']
19
+ end
20
+
21
+ # Returns true if the request's header contains "XMLHttpRequest".
22
+ def xml_http_request?
23
+ not /XMLHttpRequest/i.match(@request['headers']['X-Requested-With']).nil?
24
+ end
25
+ alias xhr? :xml_http_request?
26
+
27
+ def redirect(url_params = {},options = {})
28
+ @response['status'] = options['status'] || 302
29
+ @response['headers']['Location'] = url_for(url_params)
30
+ @response['message'] = options['message'] || 'Moved temporarily'
31
+ return ''
32
+ end
33
+
34
+ end # RhoController
35
+ end # Rho
@@ -0,0 +1,32 @@
1
+
2
+ module Rho
3
+ class RhoFSConnector
4
+
5
+ class << self
6
+
7
+ def get_app_path(appname)
8
+ File.join(__rhoGetCurrentDir(), 'apps/'+appname+'/')
9
+ end
10
+
11
+ def get_base_app_path
12
+ File.join(__rhoGetCurrentDir(), 'apps/')
13
+ end
14
+
15
+ def get_app_manifest_filename
16
+ File.join(__rhoGetCurrentDir(), 'apps/app_manifest.txt')
17
+ end
18
+
19
+ def get_model_path(appname, modelname)
20
+ File.join(__rhoGetCurrentDir(), 'apps/'+appname+'/'+modelname+'/')
21
+ end
22
+
23
+ def get_db_fullpathname
24
+ if defined? SYNC_DB_FILE
25
+ File.join(SYNC_DB_FILE)
26
+ else
27
+ File.join(__rhoGetCurrentDir(), 'db/syncdb.sqlite')
28
+ end
29
+ end
30
+ end
31
+ end # RhoApplication
32
+ end # Rho
@@ -0,0 +1,146 @@
1
+ module Rho
2
+ module RhoSupport
3
+
4
+ class << self
5
+
6
+ def url_encode(s)
7
+ s.to_s.dup.force_encoding("ASCII-8BIT").gsub(/[^a-zA-Z0-9_\-.]/n) {
8
+ sprintf("%%%02X", $&.unpack("C")[0])
9
+ }
10
+ end
11
+
12
+ def _unescape(str, regex) str.gsub(regex){ $1.hex.chr } end
13
+
14
+ ESCAPED = /%([0-9a-fA-F]{2})/
15
+
16
+ def unescape_form(str)
17
+ _unescape(str.gsub(/\+/, " "), ESCAPED)
18
+ end
19
+
20
+ def parse_query_parameters(query_string)
21
+ return {} if query_string.nil?
22
+
23
+ pairs = query_string.split('&').collect do |chunk|
24
+ next if chunk.empty?
25
+ key, value = chunk.split('=', 2)
26
+ next if key.empty?
27
+ value = value.nil? ? nil : unescape_form(value)
28
+ [ unescape_form(key), value ]
29
+ end.compact
30
+
31
+ UrlEncodedPairParser.new(pairs).result
32
+ end
33
+
34
+ def query_params(req)
35
+ params = {}
36
+ unless req['id'].nil?
37
+ params['id'] = req['id']
38
+ end
39
+ unless req['request-query'].nil? or req['request-query'].length == 0
40
+ params.merge!(parse_query_parameters(req['request-query']))
41
+ end
42
+ unless req['headers'].nil? or req['headers']['Content-Type'].nil?
43
+ if 'application/x-www-form-urlencoded'.eql? req['headers']['Content-Type']
44
+ params.merge!(parse_query_parameters(req['request-body']))
45
+ end
46
+ end
47
+ puts "Params: " + params.to_s unless params.empty?
48
+ params
49
+ end
50
+ end
51
+
52
+ class UrlEncodedPairParser < StringScanner #:nodoc:
53
+ attr_reader :top, :parent, :result
54
+
55
+ def initialize(pairs = [])
56
+ super('')
57
+ @result = {}
58
+ pairs.each { |key, value| parse(key, value) }
59
+ end
60
+
61
+ KEY_REGEXP = %r{([^\[\]=&]+)}
62
+ BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
63
+
64
+ # Parse the query string
65
+ def parse(key, value)
66
+ self.string = key
67
+ @top, @parent = result, nil
68
+
69
+ # First scan the bare key
70
+ key = scan(KEY_REGEXP) or return
71
+ key = post_key_check(key)
72
+
73
+ # Then scan as many nestings as present
74
+ until eos?
75
+ r = scan(BRACKETED_KEY_REGEXP) or return
76
+ key = self[1]
77
+ key = post_key_check(key)
78
+ end
79
+
80
+ bind(key, value)
81
+ end
82
+
83
+ private
84
+ # After we see a key, we must look ahead to determine our next action. Cases:
85
+ #
86
+ # [] follows the key. Then the value must be an array.
87
+ # = follows the key. (A value comes next)
88
+ # & or the end of string follows the key. Then the key is a flag.
89
+ # otherwise, a hash follows the key.
90
+ def post_key_check(key)
91
+ if scan(/\[\]/) # a[b][] indicates that b is an array
92
+ container(key, Array)
93
+ nil
94
+ elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
95
+ container(key, Hash)
96
+ nil
97
+ else # End of key? We do nothing.
98
+ key
99
+ end
100
+ end
101
+
102
+ # Add a container to the stack.
103
+ def container(key, klass)
104
+ type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
105
+ value = bind(key, klass.new)
106
+ type_conflict! klass, value unless value.is_a?(klass)
107
+ push(value)
108
+ end
109
+
110
+ # Push a value onto the 'stack', which is actually only the top 2 items.
111
+ def push(value)
112
+ @parent, @top = @top, value
113
+ end
114
+
115
+ # Bind a key (which may be nil for items in an array) to the provided value.
116
+ def bind(key, value)
117
+ if top.is_a? Array
118
+ if key
119
+ if top[-1].is_a?(Hash) && ! top[-1].key?(key)
120
+ top[-1][key] = value
121
+ else
122
+ top << {key => value}.with_indifferent_access
123
+ push top.last
124
+ value = top[key]
125
+ end
126
+ else
127
+ top << value
128
+ end
129
+ elsif top.is_a? Hash
130
+ #key = CGI.unescape(key)
131
+ parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
132
+ top[key] ||= value
133
+ return top[key]
134
+ else
135
+ raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
136
+ end
137
+
138
+ return value
139
+ end
140
+
141
+ def type_conflict!(klass, value)
142
+ raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
143
+ end
144
+ end
145
+ end # RhoSupport
146
+ end # Rho