mfilej-httparty 0.4.3

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 (51) hide show
  1. data/History +125 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Manifest +50 -0
  4. data/README +40 -0
  5. data/Rakefile +48 -0
  6. data/bin/httparty +98 -0
  7. data/cucumber.yml +1 -0
  8. data/examples/aaws.rb +32 -0
  9. data/examples/basic.rb +11 -0
  10. data/examples/delicious.rb +37 -0
  11. data/examples/google.rb +16 -0
  12. data/examples/rubyurl.rb +14 -0
  13. data/examples/twitter.rb +31 -0
  14. data/examples/whoismyrep.rb +10 -0
  15. data/features/basic_authentication.feature +20 -0
  16. data/features/command_line.feature +7 -0
  17. data/features/deals_with_http_error_codes.feature +26 -0
  18. data/features/handles_multiple_formats.feature +34 -0
  19. data/features/steps/env.rb +15 -0
  20. data/features/steps/httparty_response_steps.rb +26 -0
  21. data/features/steps/httparty_steps.rb +15 -0
  22. data/features/steps/mongrel_helper.rb +55 -0
  23. data/features/steps/remote_service_steps.rb +47 -0
  24. data/features/supports_redirection.feature +22 -0
  25. data/httparty.gemspec +40 -0
  26. data/lib/httparty.rb +205 -0
  27. data/lib/httparty/cookie_hash.rb +9 -0
  28. data/lib/httparty/core_extensions.rb +25 -0
  29. data/lib/httparty/exceptions.rb +7 -0
  30. data/lib/httparty/module_inheritable_attributes.rb +25 -0
  31. data/lib/httparty/request.rb +139 -0
  32. data/lib/httparty/response.rb +18 -0
  33. data/lib/httparty/version.rb +3 -0
  34. data/setup.rb +1585 -0
  35. data/spec/fixtures/delicious.xml +23 -0
  36. data/spec/fixtures/empty.xml +0 -0
  37. data/spec/fixtures/google.html +3 -0
  38. data/spec/fixtures/twitter.json +1 -0
  39. data/spec/fixtures/twitter.xml +403 -0
  40. data/spec/fixtures/undefined_method_add_node_for_nil.xml +2 -0
  41. data/spec/hash_spec.rb +49 -0
  42. data/spec/httparty/cookie_hash_spec.rb +38 -0
  43. data/spec/httparty/request_spec.rb +196 -0
  44. data/spec/httparty/response_spec.rb +62 -0
  45. data/spec/httparty_spec.rb +283 -0
  46. data/spec/spec.opts +3 -0
  47. data/spec/spec_helper.rb +21 -0
  48. data/spec/string_spec.rb +27 -0
  49. data/website/css/common.css +47 -0
  50. data/website/index.html +74 -0
  51. metadata +135 -0
@@ -0,0 +1,38 @@
1
+ require File.join(File.dirname(__FILE__), '../spec_helper')
2
+
3
+ describe HTTParty::CookieHash do
4
+ before(:each) do
5
+ @cookie_hash = HTTParty::CookieHash.new
6
+ end
7
+
8
+ describe "#add_cookies" do
9
+ it "should add new key/value pairs to the hash" do
10
+ @cookie_hash.add_cookies(:foo => "bar")
11
+ @cookie_hash.add_cookies(:rofl => "copter")
12
+ @cookie_hash.length.should eql(2)
13
+ end
14
+
15
+ it "should overwrite any existing key" do
16
+ @cookie_hash.add_cookies(:foo => "bar")
17
+ @cookie_hash.add_cookies(:foo => "copter")
18
+ @cookie_hash.length.should eql(1)
19
+ @cookie_hash[:foo].should eql("copter")
20
+ end
21
+ end
22
+
23
+ # The regexen are required because Hashes aren't ordered, so a test against
24
+ # a hardcoded string was randomly failing.
25
+ describe "#to_cookie_string" do
26
+ before(:each) do
27
+ @cookie_hash.add_cookies(:foo => "bar")
28
+ @cookie_hash.add_cookies(:rofl => "copter")
29
+ @s = @cookie_hash.to_cookie_string
30
+ end
31
+
32
+ it "should format the key/value pairs, delimited by semi-colons" do
33
+ @s.should match(/foo=bar/)
34
+ @s.should match(/rofl=copter/)
35
+ @s.should match(/^\w+=\w+; \w+=\w+$/)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,196 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe HTTParty::Request do
4
+ def stub_response(body, code = 200)
5
+ unless @http
6
+ @http = Net::HTTP.new('localhost', 80)
7
+ @request.stub!(:http).and_return(@http)
8
+ @request.stub!(:uri).and_return(URI.parse("http://foo.com/foobar"))
9
+ end
10
+
11
+ response = Net::HTTPResponse::CODE_TO_OBJ[code.to_s].new("1.1", code, body)
12
+ response.stub!(:body).and_return(body)
13
+
14
+ @http.stub!(:request).and_return(response)
15
+ response
16
+ end
17
+
18
+ before do
19
+ @request = HTTParty::Request.new(Net::HTTP::Get, 'http://api.foo.com/v1', :format => :xml)
20
+ end
21
+
22
+ describe "#format" do
23
+ it "should return the correct parsing format" do
24
+ @request.format.should == :xml
25
+ end
26
+ end
27
+
28
+ describe 'http' do
29
+ it "should use ssl for port 443" do
30
+ request = HTTParty::Request.new(Net::HTTP::Get, 'https://api.foo.com/v1:443')
31
+ request.send(:http).use_ssl?.should == true
32
+ end
33
+
34
+ it 'should not use ssl for port 80' do
35
+ request = HTTParty::Request.new(Net::HTTP::Get, 'http://foobar.com')
36
+ @request.send(:http).use_ssl?.should == false
37
+ end
38
+
39
+ it "should use basic auth when configured" do
40
+ @request.options[:basic_auth] = {:username => 'foobar', :password => 'secret'}
41
+ @request.send(:setup_raw_request)
42
+ @request.instance_variable_get(:@raw_request)['authorization'].should_not be_nil
43
+ end
44
+ end
45
+
46
+ describe '#format_from_mimetype' do
47
+ it 'should handle text/xml' do
48
+ ["text/xml", "text/xml; charset=iso8859-1"].each do |ct|
49
+ @request.send(:format_from_mimetype, ct).should == :xml
50
+ end
51
+ end
52
+
53
+ it 'should handle application/xml' do
54
+ ["application/xml", "application/xml; charset=iso8859-1"].each do |ct|
55
+ @request.send(:format_from_mimetype, ct).should == :xml
56
+ end
57
+ end
58
+
59
+ it 'should handle text/json' do
60
+ ["text/json", "text/json; charset=iso8859-1"].each do |ct|
61
+ @request.send(:format_from_mimetype, ct).should == :json
62
+ end
63
+ end
64
+
65
+ it 'should handle application/json' do
66
+ ["application/json", "application/json; charset=iso8859-1"].each do |ct|
67
+ @request.send(:format_from_mimetype, ct).should == :json
68
+ end
69
+ end
70
+
71
+ it 'should handle text/javascript' do
72
+ ["text/javascript", "text/javascript; charset=iso8859-1"].each do |ct|
73
+ @request.send(:format_from_mimetype, ct).should == :json
74
+ end
75
+ end
76
+
77
+ it 'should handle application/javascript' do
78
+ ["application/javascript", "application/javascript; charset=iso8859-1"].each do |ct|
79
+ @request.send(:format_from_mimetype, ct).should == :json
80
+ end
81
+ end
82
+ end
83
+
84
+ describe 'parsing responses' do
85
+ it 'should handle xml automatically' do
86
+ xml = %q[<books><book><id>1234</id><name>Foo Bar!</name></book></books>]
87
+ @request.options[:format] = :xml
88
+ @request.send(:parse_response, xml).should == {'books' => {'book' => {'id' => '1234', 'name' => 'Foo Bar!'}}}
89
+ end
90
+
91
+ it 'should handle json automatically' do
92
+ json = %q[{"books": {"book": {"name": "Foo Bar!", "id": "1234"}}}]
93
+ @request.options[:format] = :json
94
+ @request.send(:parse_response, json).should == {'books' => {'book' => {'id' => '1234', 'name' => 'Foo Bar!'}}}
95
+ end
96
+
97
+ it 'should handle yaml automatically' do
98
+ yaml = "books: \n book: \n name: Foo Bar!\n id: \"1234\"\n"
99
+ @request.options[:format] = :yaml
100
+ @request.send(:parse_response, yaml).should == {'books' => {'book' => {'id' => '1234', 'name' => 'Foo Bar!'}}}
101
+ end
102
+
103
+ it "should include any HTTP headers in the returned response" do
104
+ @request.options[:format] = :html
105
+ response = stub_response "Content"
106
+ response.initialize_http_header("key" => "value")
107
+
108
+ @request.perform.headers.should == { "key" => ["value"] }
109
+ end
110
+
111
+ describe 'with non-200 responses' do
112
+
113
+ it 'should return a valid object for 4xx response' do
114
+ stub_response '<foo><bar>yes</bar></foo>', 401
115
+ resp = @request.perform
116
+ resp.code.should == 401
117
+ resp.body.should == "<foo><bar>yes</bar></foo>"
118
+ resp['foo']['bar'].should == "yes"
119
+ end
120
+
121
+ it 'should return a valid object for 5xx response' do
122
+ stub_response '<foo><bar>error</bar></foo>', 500
123
+ resp = @request.perform
124
+ resp.code.should == 500
125
+ resp.body.should == "<foo><bar>error</bar></foo>"
126
+ resp['foo']['bar'].should == "error"
127
+ end
128
+
129
+ end
130
+ end
131
+
132
+ it "should not attempt to parse empty responses" do
133
+ stub_response "", 204
134
+
135
+ @request.options[:format] = :xml
136
+ @request.perform.should be_nil
137
+ end
138
+
139
+ it "should not fail for missing mime type" do
140
+ stub_response "Content for you"
141
+ @request.options[:format] = :html
142
+ @request.perform.should == 'Content for you'
143
+ end
144
+
145
+ describe "a request that redirects" do
146
+ before(:each) do
147
+ @redirect = stub_response("", 302)
148
+ @redirect['location'] = '/foo'
149
+
150
+ @ok = stub_response('<hash><foo>bar</foo></hash>', 200)
151
+ end
152
+
153
+ describe "once" do
154
+ before(:each) do
155
+ @http.stub!(:request).and_return(@redirect, @ok)
156
+ end
157
+
158
+ it "should be handled by GET transparently" do
159
+ @request.perform.should == {"hash" => {"foo" => "bar"}}
160
+ end
161
+
162
+ it "should be handled by POST transparently" do
163
+ @request.http_method = Net::HTTP::Post
164
+ @request.perform.should == {"hash" => {"foo" => "bar"}}
165
+ end
166
+
167
+ it "should be handled by DELETE transparently" do
168
+ @request.http_method = Net::HTTP::Delete
169
+ @request.perform.should == {"hash" => {"foo" => "bar"}}
170
+ end
171
+
172
+ it "should be handled by PUT transparently" do
173
+ @request.http_method = Net::HTTP::Put
174
+ @request.perform.should == {"hash" => {"foo" => "bar"}}
175
+ end
176
+ end
177
+
178
+ describe "infinitely" do
179
+ before(:each) do
180
+ @http.stub!(:request).and_return(@redirect)
181
+ end
182
+
183
+ it "should raise an exception" do
184
+ lambda { @request.perform }.should raise_error(HTTParty::RedirectionTooDeep)
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ describe HTTParty::Request, "with POST http method" do
191
+ it "should raise argument error if query is not a hash" do
192
+ lambda {
193
+ HTTParty::Request.new(Net::HTTP::Post, 'http://api.foo.com/v1', :format => :xml, :query => 'astring').perform
194
+ }.should raise_error(ArgumentError)
195
+ end
196
+ end
@@ -0,0 +1,62 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe HTTParty::Response do
4
+ describe "initialization" do
5
+ before do
6
+ @response_object = {'foo' => 'bar'}
7
+ @body = "{foo:'bar'}"
8
+ @code = '200'
9
+ @message = 'OK'
10
+ @response = HTTParty::Response.new(@response_object, @body, @code, @message)
11
+ end
12
+
13
+ it "should set delegate" do
14
+ @response.delegate.should == @response_object
15
+ end
16
+
17
+ it "should set body" do
18
+ @response.body.should == @body
19
+ end
20
+
21
+ it "should set code" do
22
+ @response.code.should.to_s == @code
23
+ end
24
+
25
+ it "should set code as a Fixnum" do
26
+ @response.code.should be_an_instance_of(Fixnum)
27
+ end
28
+
29
+ it "should set body" do
30
+ @response.body.should == @body
31
+ end
32
+ end
33
+
34
+ it "should be able to set headers during initialization" do
35
+ response = HTTParty::Response.new({'foo' => 'bar'}, "{foo:'bar'}", 200, 'OK', {'foo' => 'bar'})
36
+ response.headers.should == {'foo' => 'bar'}
37
+ end
38
+
39
+ it "should send missing methods to delegate" do
40
+ response = HTTParty::Response.new({'foo' => 'bar'}, "{foo:'bar'}", 200, 'OK')
41
+ response['foo'].should == 'bar'
42
+ end
43
+
44
+ it "should be able to iterate delegate if it is array" do
45
+ response = HTTParty::Response.new([{'foo' => 'bar'}, {'foo' => 'baz'}], "[{foo:'bar'}, {foo:'baz'}]", 200, 'OK')
46
+ response.size.should == 2
47
+ lambda {
48
+ response.each { |item| }
49
+ }.should_not raise_error
50
+ end
51
+
52
+ xit "should allow hashes to be accessed with dot notation" do
53
+ response = HTTParty::Response.new({'foo' => 'bar'}, "{foo:'bar'}", 200, 'OK')
54
+ response.foo.should == 'bar'
55
+ end
56
+
57
+ xit "should allow nested hashes to be accessed with dot notation" do
58
+ response = HTTParty::Response.new({'foo' => {'bar' => 'baz'}}, "{foo: {bar:'baz'}}", 200, 'OK')
59
+ response.foo.should == {'bar' => 'baz'}
60
+ response.foo.bar.should == 'baz'
61
+ end
62
+ end
@@ -0,0 +1,283 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe HTTParty do
4
+ before(:each) do
5
+ @klass = Class.new
6
+ @klass.instance_eval { include HTTParty }
7
+ end
8
+
9
+ describe "base uri" do
10
+ before(:each) do
11
+ @klass.base_uri('api.foo.com/v1')
12
+ end
13
+
14
+ it "should have reader" do
15
+ @klass.base_uri.should == 'http://api.foo.com/v1'
16
+ end
17
+
18
+ it 'should have writer' do
19
+ @klass.base_uri('http://api.foobar.com')
20
+ @klass.base_uri.should == 'http://api.foobar.com'
21
+ end
22
+
23
+ it 'should not modify the parameter during assignment' do
24
+ uri = 'http://api.foobar.com'
25
+ @klass.base_uri(uri)
26
+ uri.should == 'http://api.foobar.com'
27
+ end
28
+ end
29
+
30
+ describe "#normalize_base_uri" do
31
+ it "should add http if not present for non ssl requests" do
32
+ uri = HTTParty.normalize_base_uri('api.foobar.com')
33
+ uri.should == 'http://api.foobar.com'
34
+ end
35
+
36
+ it "should add https if not present for ssl requests" do
37
+ uri = HTTParty.normalize_base_uri('api.foo.com/v1:443')
38
+ uri.should == 'https://api.foo.com/v1:443'
39
+ end
40
+
41
+ it "should not remove https for ssl requests" do
42
+ uri = HTTParty.normalize_base_uri('https://api.foo.com/v1:443')
43
+ uri.should == 'https://api.foo.com/v1:443'
44
+ end
45
+
46
+ it 'should not modify the parameter' do
47
+ uri = 'http://api.foobar.com'
48
+ HTTParty.normalize_base_uri(uri)
49
+ uri.should == 'http://api.foobar.com'
50
+ end
51
+ end
52
+
53
+ describe "headers" do
54
+ it "should default to empty hash" do
55
+ @klass.headers.should == {}
56
+ end
57
+
58
+ it "should be able to be updated" do
59
+ init_headers = {:foo => 'bar', :baz => 'spax'}
60
+ @klass.headers init_headers
61
+ @klass.headers.should == init_headers
62
+ end
63
+ end
64
+
65
+ describe "cookies" do
66
+ def expect_cookie_header(s)
67
+ HTTParty::Request.should_receive(:new) \
68
+ .with(anything, anything, hash_including({ :headers => { "cookie" => s } })) \
69
+ .and_return(mock("mock response", :perform => nil))
70
+ end
71
+
72
+ it "should not be in the headers by default" do
73
+ HTTParty::Request.stub!(:new).and_return(stub(nil, :perform => nil))
74
+ @klass.get("")
75
+ @klass.headers.keys.should_not include("cookie")
76
+ end
77
+
78
+ it "should raise an ArgumentError if passed a non-Hash" do
79
+ lambda do
80
+ @klass.cookies("nonsense")
81
+ end.should raise_error(ArgumentError)
82
+ end
83
+
84
+ it "should allow a cookie to be specified with a one-off request" do
85
+ expect_cookie_header "type=snickerdoodle"
86
+ @klass.get("", :cookies => { :type => "snickerdoodle" })
87
+ end
88
+
89
+ describe "when a cookie is set at the class level" do
90
+ before(:each) do
91
+ @klass.cookies({ :type => "snickerdoodle" })
92
+ end
93
+
94
+ it "should include that cookie in the request" do
95
+ expect_cookie_header "type=snickerdoodle"
96
+ @klass.get("")
97
+ end
98
+
99
+ it "should allow the class defaults to be overridden" do
100
+ expect_cookie_header "type=chocolate_chip"
101
+
102
+ @klass.get("", :cookies => { :type => "chocolate_chip" })
103
+ end
104
+ end
105
+
106
+ describe "in a class with multiple methods that use different cookies" do
107
+ before(:each) do
108
+ @klass.instance_eval do
109
+ def first_method
110
+ get("first_method", :cookies => { :first_method_cookie => "foo" })
111
+ end
112
+
113
+ def second_method
114
+ get("second_method", :cookies => { :second_method_cookie => "foo" })
115
+ end
116
+ end
117
+ end
118
+
119
+ it "should not allow cookies used in one method to carry over into other methods" do
120
+ expect_cookie_header "first_method_cookie=foo"
121
+ @klass.first_method
122
+
123
+ expect_cookie_header "second_method_cookie=foo"
124
+ @klass.second_method
125
+ end
126
+ end
127
+ end
128
+
129
+ describe "default params" do
130
+ it "should default to empty hash" do
131
+ @klass.default_params.should == {}
132
+ end
133
+
134
+ it "should be able to be updated" do
135
+ new_defaults = {:foo => 'bar', :baz => 'spax'}
136
+ @klass.default_params new_defaults
137
+ @klass.default_params.should == new_defaults
138
+ end
139
+ end
140
+
141
+ describe "basic http authentication" do
142
+ it "should work" do
143
+ @klass.basic_auth 'foobar', 'secret'
144
+ @klass.default_options[:basic_auth].should == {:username => 'foobar', :password => 'secret'}
145
+ end
146
+ end
147
+
148
+ describe "format" do
149
+ it "should allow xml" do
150
+ @klass.format :xml
151
+ @klass.default_options[:format].should == :xml
152
+ end
153
+
154
+ it "should allow json" do
155
+ @klass.format :json
156
+ @klass.default_options[:format].should == :json
157
+ end
158
+
159
+ it "should allow yaml" do
160
+ @klass.format :yaml
161
+ @klass.default_options[:format].should == :yaml
162
+ end
163
+
164
+ it "should allow plain" do
165
+ @klass.format :plain
166
+ @klass.default_options[:format].should == :plain
167
+ end
168
+
169
+ it 'should not allow funky format' do
170
+ lambda do
171
+ @klass.format :foobar
172
+ end.should raise_error(HTTParty::UnsupportedFormat)
173
+ end
174
+
175
+ it 'should only print each format once with an exception' do
176
+ lambda do
177
+ @klass.format :foobar
178
+ end.should raise_error(HTTParty::UnsupportedFormat, "Must be one of: json, plain, html, yaml, xml")
179
+ end
180
+
181
+ end
182
+
183
+ describe "with explicit override of automatic redirect handling" do
184
+
185
+ it "should fail with redirected GET" do
186
+ lambda do
187
+ @klass.get('/foo', :no_follow => true)
188
+ end.should raise_error(HTTParty::RedirectionTooDeep)
189
+ end
190
+
191
+ it "should fail with redirected POST" do
192
+ lambda do
193
+ @klass.post('/foo', :no_follow => true)
194
+ end.should raise_error(HTTParty::RedirectionTooDeep)
195
+ end
196
+
197
+ it "should fail with redirected DELETE" do
198
+ lambda do
199
+ @klass.delete('/foo', :no_follow => true)
200
+ end.should raise_error(HTTParty::RedirectionTooDeep)
201
+ end
202
+
203
+ it "should fail with redirected PUT" do
204
+ lambda do
205
+ @klass.put('/foo', :no_follow => true)
206
+ end.should raise_error(HTTParty::RedirectionTooDeep)
207
+ end
208
+ end
209
+
210
+ describe "with multiple class definitions" do
211
+ before(:each) do
212
+ @klass.instance_eval do
213
+ base_uri "http://first.com"
214
+ default_params :one => 1
215
+ end
216
+
217
+ @additional_klass = Class.new
218
+ @additional_klass.instance_eval do
219
+ include HTTParty
220
+ base_uri "http://second.com"
221
+ default_params :two => 2
222
+ end
223
+ end
224
+
225
+ it "should not run over each others options" do
226
+ @klass.default_options.should == { :base_uri => 'http://first.com', :default_params => { :one => 1 } }
227
+ @additional_klass.default_options.should == { :base_uri => 'http://second.com', :default_params => { :two => 2 } }
228
+ end
229
+ end
230
+
231
+ describe "#get" do
232
+ it "should be able to get html" do
233
+ stub_http_response_with('google.html')
234
+ HTTParty.get('http://www.google.com').should == file_fixture('google.html')
235
+ end
236
+
237
+ it "should be able parse response type json automatically" do
238
+ stub_http_response_with('twitter.json')
239
+ tweets = HTTParty.get('http://twitter.com/statuses/public_timeline.json')
240
+ tweets.size.should == 20
241
+ tweets.first['user'].should == {
242
+ "name" => "Pyk",
243
+ "url" => nil,
244
+ "id" => "7694602",
245
+ "description" => nil,
246
+ "protected" => false,
247
+ "screen_name" => "Pyk",
248
+ "followers_count" => 1,
249
+ "location" => "Opera Plaza, California",
250
+ "profile_image_url" => "http://static.twitter.com/images/default_profile_normal.png"
251
+ }
252
+ end
253
+
254
+ it "should be able parse response type xml automatically" do
255
+ stub_http_response_with('twitter.xml')
256
+ tweets = HTTParty.get('http://twitter.com/statuses/public_timeline.xml')
257
+ tweets['statuses'].size.should == 20
258
+ tweets['statuses'].first['user'].should == {
259
+ "name" => "Magic 8 Bot",
260
+ "url" => nil,
261
+ "id" => "17656026",
262
+ "description" => "ask me a question",
263
+ "protected" => "false",
264
+ "screen_name" => "magic8bot",
265
+ "followers_count" => "90",
266
+ "profile_image_url" => "http://s3.amazonaws.com/twitter_production/profile_images/65565851/8ball_large_normal.jpg",
267
+ "location" => nil
268
+ }
269
+ end
270
+
271
+ it "should not get undefined method add_node for nil class for the following xml" do
272
+ stub_http_response_with('undefined_method_add_node_for_nil.xml')
273
+ result = HTTParty.get('http://foobar.com')
274
+ result.should == {"Entities"=>{"href"=>"https://s3-sandbox.parature.com/api/v1/5578/5633/Account", "results"=>"0", "total"=>"0", "page_size"=>"25", "page"=>"1"}}
275
+ end
276
+
277
+ it "should parse empty response fine" do
278
+ stub_http_response_with('empty.xml')
279
+ result = HTTParty.get('http://foobar.com')
280
+ result.should == nil
281
+ end
282
+ end
283
+ end