rhodes-framework 1.0.0

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