alexvollmer-httparty 0.2.6 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. data/History +34 -0
  2. data/Manifest +25 -2
  3. data/README +2 -2
  4. data/Rakefile +6 -2
  5. data/bin/httparty +39 -44
  6. data/cucumber.yml +1 -0
  7. data/examples/basic.rb +6 -1
  8. data/examples/delicious.rb +1 -0
  9. data/examples/rubyurl.rb +1 -1
  10. data/features/basic_authentication.feature +20 -0
  11. data/features/command_line.feature +7 -0
  12. data/features/deals_with_http_error_codes.feature +26 -0
  13. data/features/handles_multiple_formats.feature +34 -0
  14. data/features/steps/env.rb +15 -0
  15. data/features/steps/httparty_response_steps.rb +26 -0
  16. data/features/steps/httparty_steps.rb +15 -0
  17. data/features/steps/mongrel_helper.rb +55 -0
  18. data/features/steps/remote_service_steps.rb +47 -0
  19. data/features/supports_redirection.feature +22 -0
  20. data/httparty.gemspec +4 -7
  21. data/lib/core_extensions.rb +48 -222
  22. data/lib/httparty/cookie_hash.rb +9 -0
  23. data/lib/httparty/exceptions.rb +3 -0
  24. data/lib/httparty/module_inheritable_attributes.rb +25 -0
  25. data/lib/httparty/parsers/json.rb +74 -0
  26. data/lib/httparty/parsers/xml.rb +209 -0
  27. data/lib/httparty/parsers.rb +4 -0
  28. data/lib/httparty/request.rb +63 -76
  29. data/lib/httparty/response.rb +17 -0
  30. data/lib/httparty/version.rb +2 -2
  31. data/lib/httparty.rb +108 -19
  32. data/spec/fixtures/empty.xml +0 -0
  33. data/spec/fixtures/undefined_method_add_node_for_nil.xml +2 -0
  34. data/spec/hash_spec.rb +49 -0
  35. data/spec/httparty/cookie_hash_spec.rb +38 -0
  36. data/spec/httparty/parsers/json_spec.rb +42 -0
  37. data/spec/httparty/parsers/xml_spec.rb +445 -0
  38. data/spec/httparty/request_spec.rb +219 -80
  39. data/spec/httparty/response_spec.rb +53 -0
  40. data/spec/httparty_spec.rb +125 -64
  41. data/spec/spec_helper.rb +5 -8
  42. data/spec/string_spec.rb +27 -0
  43. metadata +34 -14
  44. data/lib/module_level_inheritable_attributes.rb +0 -25
  45. data/spec/as_buggery_spec.rb +0 -16
@@ -11,8 +11,7 @@ module HTTParty
11
11
  self.path = path
12
12
  self.options = {
13
13
  :limit => o.delete(:no_follow) ? 0 : 5,
14
- :default_params => {},
15
- :keep_body => false
14
+ :default_params => {}
16
15
  }.merge(o)
17
16
  end
18
17
 
@@ -21,9 +20,13 @@ module HTTParty
21
20
  end
22
21
 
23
22
  def uri
24
- uri = path.relative? ? URI.parse("#{options[:base_uri]}#{path}") : path
25
- uri.query = query_string(uri)
26
- uri
23
+ new_uri = path.relative? ? URI.parse("#{options[:base_uri]}#{path}") : path
24
+
25
+ if Net::HTTP::Get === http_method and !@redirect
26
+ new_uri.query = query_string(new_uri)
27
+ end
28
+
29
+ new_uri
27
30
  end
28
31
 
29
32
  def format
@@ -31,38 +34,47 @@ module HTTParty
31
34
  end
32
35
 
33
36
  def perform
34
- validate!
35
- handle_response!(get_response(uri))
37
+ validate
38
+ setup_raw_request
39
+ handle_response(get_response)
36
40
  end
37
41
 
38
42
  private
39
- def http(uri) #:nodoc:
43
+ def http
40
44
  http = Net::HTTP.new(uri.host, uri.port, options[:http_proxyaddr], options[:http_proxyport])
41
45
  http.use_ssl = (uri.port == 443)
42
46
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
43
47
  http
44
48
  end
45
49
 
46
- def get_response(uri) #:nodoc:
47
- request = http_method.new(uri.request_uri)
50
+ def configure_basic_auth
51
+ @raw_request.basic_auth(options[:basic_auth][:username], options[:basic_auth][:password])
52
+ end
53
+
54
+ def setup_raw_request
55
+ @raw_request = http_method.new(uri.request_uri)
48
56
 
49
57
  if post? && options[:query]
50
- request.set_form_data(options[:query])
58
+ @raw_request.set_form_data(options[:query])
51
59
  end
60
+
61
+ @raw_request.body = options[:body].is_a?(Hash) ? options[:body].to_params : options[:body] unless options[:body].blank?
62
+ @raw_request.initialize_http_header options[:headers]
63
+
64
+ configure_basic_auth if options[:basic_auth]
65
+ end
52
66
 
53
- request.body = options[:body].is_a?(Hash) ? options[:body].to_params : options[:body] unless options[:body].blank?
54
- request.initialize_http_header options[:headers]
55
-
56
- if options[:basic_auth]
57
- request.basic_auth(options[:basic_auth][:username], options[:basic_auth][:password])
58
- end
67
+ def perform_actual_request
68
+ http.request(@raw_request)
69
+ end
59
70
 
60
- response = http(uri).request(request)
71
+ def get_response
72
+ response = perform_actual_request
61
73
  options[:format] ||= format_from_mimetype(response['content-type'])
62
74
  response
63
75
  end
64
-
65
- def query_string(uri) #:nodoc:
76
+
77
+ def query_string(uri)
66
78
  query_string_parts = []
67
79
  query_string_parts << uri.query unless uri.query.blank?
68
80
 
@@ -75,48 +87,47 @@ module HTTParty
75
87
 
76
88
  query_string_parts.size > 0 ? query_string_parts.join('&') : nil
77
89
  end
78
-
90
+
79
91
  # Raises exception Net::XXX (http error code) if an http error occured
80
- def handle_response!(response) #:nodoc:
92
+ def handle_response(response)
81
93
  case response
82
- when Net::HTTPSuccess
83
- original_resp = parse_response(response.body)
84
- final_resp = Response.new(response.body, nil, original_resp)
85
- response.each_header { |n,v| final_resp.add_header(n, v) }
86
- final_resp
87
- when Net::HTTPRedirection
88
- options[:limit] -= 1
89
- self.path = response['location']
90
- perform
91
- else
92
- response.instance_eval { class << self; attr_accessor :body_parsed; end }
93
- begin; response.body_parsed = parse_response(response.body); rescue; end
94
- response.error! # raises exception corresponding to http error Net::XXX
95
- end
94
+ when Net::HTTPRedirection
95
+ options[:limit] -= 1
96
+ if options[:limit].to_i <= 0
97
+ raise HTTParty::RedirectionTooDeep, 'HTTP redirects too deep'
98
+ end
99
+ self.http_method = Net::HTTP::Get
100
+ self.path = response['location']
101
+ @redirect = true
102
+ perform
103
+ else
104
+ parsed_response = parse_response(response.body)
105
+ Response.new(parsed_response, response.body, response.code, response.to_hash)
106
+ end
96
107
  end
97
-
98
- def parse_response(body) #:nodoc:
108
+
109
+ def parse_response(body)
99
110
  return nil if body.nil? or body.empty?
100
- response = case format
101
- when :xml
102
- ToHashParser.from_xml(body)
103
- when :json
104
- JSON.parse(body)
105
- else
106
- body
107
- end
108
- response
111
+ case format
112
+ when :xml
113
+ HTTParty::Parsers::XML.parse(body)
114
+ when :json
115
+ HTTParty::Parsers::JSON.decode(body)
116
+ when :yaml
117
+ YAML::load(body)
118
+ else
119
+ body
120
+ end
109
121
  end
110
122
 
111
123
  # Uses the HTTP Content-Type header to determine the format of the response
112
124
  # It compares the MIME type returned to the types stored in the AllowedFormats hash
113
- def format_from_mimetype(mimetype) #:nodoc:
125
+ def format_from_mimetype(mimetype)
114
126
  return nil if mimetype.nil?
115
- AllowedFormats.each { |k, v| return k if mimetype.include?(v) }
127
+ AllowedFormats.each { |k, v| return v if mimetype.include?(k) }
116
128
  end
117
-
118
- def validate! #:nodoc:
119
- raise HTTParty::RedirectionTooDeep, 'HTTP redirects too deep' if options[:limit].to_i <= 0
129
+
130
+ def validate
120
131
  raise ArgumentError, 'only get, post, put and delete methods are supported' unless SupportedHTTPMethods.include?(http_method)
121
132
  raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].is_a?(Hash)
122
133
  raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].is_a?(Hash)
@@ -127,28 +138,4 @@ module HTTParty
127
138
  Net::HTTP::Post == http_method
128
139
  end
129
140
  end
130
-
131
- class Response
132
- attr_accessor :original_body, :format, :headers
133
-
134
- def initialize(original_body, format, delegate)
135
- self.original_body = original_body
136
- self.format = format
137
- self.headers = { }
138
- @delegate = delegate
139
- end
140
-
141
- def add_header(name, value)
142
- @headers[name] = value
143
- end
144
-
145
- def method_missing(sym, *args)
146
- @delegate.send(sym, *args)
147
- end
148
-
149
- def pretty_print(pp)
150
- @delegate.pretty_print(pp)
151
- end
152
-
153
- end
154
141
  end
@@ -0,0 +1,17 @@
1
+ module HTTParty
2
+ class Response < BlankSlate #:nodoc:
3
+ attr_accessor :body, :code, :headers
4
+ attr_reader :delegate
5
+
6
+ def initialize(delegate, body, code, headers={})
7
+ @delegate = delegate
8
+ @body = body
9
+ @code = code
10
+ @headers = headers
11
+ end
12
+
13
+ def method_missing(name, *args, &block)
14
+ @delegate.send(name, *args, &block)
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
- module HTTParty
2
- Version = '0.2.6'
1
+ module HTTParty #:nodoc:
2
+ Version = '0.3.1'
3
3
  end
data/lib/httparty.rb CHANGED
@@ -2,63 +2,135 @@ $:.unshift(File.dirname(__FILE__))
2
2
 
3
3
  require 'net/http'
4
4
  require 'net/https'
5
- require 'rubygems'
6
- gem 'json', '>= 1.1.3'
7
- require 'json'
8
-
9
- require 'module_level_inheritable_attributes'
10
5
  require 'core_extensions'
6
+ require 'httparty/module_inheritable_attributes'
11
7
 
12
- module HTTParty
13
- AllowedFormats = {:xml => 'text/xml', :json => 'application/json', :html => 'text/html'}
8
+ module HTTParty
9
+
10
+ AllowedFormats = {
11
+ 'text/xml' => :xml,
12
+ 'application/xml' => :xml,
13
+ 'application/json' => :json,
14
+ 'text/json' => :json,
15
+ 'application/javascript' => :json,
16
+ 'text/javascript' => :json,
17
+ 'text/html' => :html,
18
+ 'application/x-yaml' => :yaml,
19
+ 'text/yaml' => :yaml
20
+ } unless defined?(AllowedFormats)
14
21
 
15
22
  def self.included(base)
16
23
  base.extend ClassMethods
17
- base.send :include, ModuleLevelInheritableAttributes
24
+ base.send :include, HTTParty::ModuleInheritableAttributes
18
25
  base.send(:mattr_inheritable, :default_options)
19
26
  base.instance_variable_set("@default_options", {})
20
27
  end
21
28
 
22
29
  module ClassMethods
23
- def default_options
24
- @default_options
25
- end
26
-
30
+ # Allows setting http proxy information to be used
31
+ #
32
+ # class Foo
33
+ # include HTTParty
34
+ # http_proxy 'http://foo.com', 80
35
+ # end
27
36
  def http_proxy(addr=nil, port = nil)
28
37
  default_options[:http_proxyaddr] = addr
29
38
  default_options[:http_proxyport] = port
30
39
  end
31
-
40
+
41
+ # Allows setting a base uri to be used for each request.
42
+ # Will normalize uri to include http, etc.
43
+ #
44
+ # class Foo
45
+ # include HTTParty
46
+ # base_uri 'twitter.com'
47
+ # end
32
48
  def base_uri(uri=nil)
33
49
  return default_options[:base_uri] unless uri
34
50
  default_options[:base_uri] = HTTParty.normalize_base_uri(uri)
35
51
  end
36
-
52
+
53
+ # Allows setting basic authentication username and password.
54
+ #
55
+ # class Foo
56
+ # include HTTParty
57
+ # basic_auth 'username', 'password'
58
+ # end
37
59
  def basic_auth(u, p)
38
60
  default_options[:basic_auth] = {:username => u, :password => p}
39
61
  end
40
62
 
63
+ # Allows setting default parameters to be appended to each request.
64
+ # Great for api keys and such.
65
+ #
66
+ # class Foo
67
+ # include HTTParty
68
+ # default_params :api_key => 'secret', :another => 'foo'
69
+ # end
41
70
  def default_params(h={})
42
71
  raise ArgumentError, 'Default params must be a hash' unless h.is_a?(Hash)
43
72
  default_options[:default_params] ||= {}
44
73
  default_options[:default_params].merge!(h)
45
74
  end
46
-
75
+
76
+ # Allows setting a base uri to be used for each request.
77
+ #
78
+ # class Foo
79
+ # include HTTParty
80
+ # headers 'Accept' => 'text/html'
81
+ # end
47
82
  def headers(h={})
48
83
  raise ArgumentError, 'Headers must be a hash' unless h.is_a?(Hash)
49
84
  default_options[:headers] ||= {}
50
85
  default_options[:headers].merge!(h)
51
86
  end
87
+
88
+ def cookies(h={})
89
+ raise ArgumentError, 'Cookies must be a hash' unless h.is_a?(Hash)
90
+ default_options[:cookies] ||= CookieHash.new
91
+ default_options[:cookies].add_cookies(h)
92
+ end
52
93
 
94
+ # Allows setting the format with which to parse.
95
+ # Must be one of the allowed formats ie: json, xml
96
+ #
97
+ # class Foo
98
+ # include HTTParty
99
+ # format :json
100
+ # end
53
101
  def format(f)
54
- raise UnsupportedFormat, "Must be one of: #{AllowedFormats.keys.join(', ')}" unless AllowedFormats.key?(f)
102
+ raise UnsupportedFormat, "Must be one of: #{AllowedFormats.values.join(', ')}" unless AllowedFormats.value?(f)
55
103
  default_options[:format] = f
56
104
  end
57
105
 
106
+ # Allows making a get request to a url.
107
+ #
108
+ # class Foo
109
+ # include HTTParty
110
+ # end
111
+ #
112
+ # # Simple get with full url
113
+ # Foo.get('http://foo.com/resource.json')
114
+ #
115
+ # # Simple get with full url and query parameters
116
+ # # ie: http://foo.com/resource.json?limit=10
117
+ # Foo.get('http://foo.com/resource.json', :query => {:limit => 10})
58
118
  def get(path, options={})
59
119
  perform_request Net::HTTP::Get, path, options
60
120
  end
61
-
121
+
122
+ # Allows making a post request to a url.
123
+ #
124
+ # class Foo
125
+ # include HTTParty
126
+ # end
127
+ #
128
+ # # Simple post with full url and setting the body
129
+ # Foo.post('http://foo.com/resources', :body => {:bar => 'baz'})
130
+ #
131
+ # # Simple post with full url using :query option,
132
+ # # which gets set as form data on the request.
133
+ # Foo.post('http://foo.com/resources', :query => {:bar => 'baz'})
62
134
  def post(path, options={})
63
135
  perform_request Net::HTTP::Post, path, options
64
136
  end
@@ -70,11 +142,25 @@ module HTTParty
70
142
  def delete(path, options={})
71
143
  perform_request Net::HTTP::Delete, path, options
72
144
  end
145
+
146
+ def default_options #:nodoc:
147
+ @default_options
148
+ end
73
149
 
74
150
  private
75
151
  def perform_request(http_method, path, options) #:nodoc:
152
+ process_cookies(options)
76
153
  Request.new(http_method, path, default_options.dup.merge(options)).perform
77
154
  end
155
+
156
+ def process_cookies(options) #:nodoc:
157
+ return unless options[:cookies] || default_options[:cookies]
158
+ options[:headers] ||= {}
159
+ options[:headers]["cookie"] = cookies(options[:cookies] || {}).to_cookie_string
160
+
161
+ default_options.delete(:cookies)
162
+ options.delete(:cookies)
163
+ end
78
164
  end
79
165
 
80
166
  def self.normalize_base_uri(url) #:nodoc:
@@ -87,7 +173,7 @@ module HTTParty
87
173
  "http#{'s' if use_ssl}://#{url}"
88
174
  end
89
175
 
90
- class Basement
176
+ class Basement #:nodoc:
91
177
  include HTTParty
92
178
  end
93
179
 
@@ -108,5 +194,8 @@ module HTTParty
108
194
  end
109
195
  end
110
196
 
197
+ require 'httparty/cookie_hash'
111
198
  require 'httparty/exceptions'
112
- require 'httparty/request'
199
+ require 'httparty/request'
200
+ require 'httparty/response'
201
+ require 'httparty/parsers'
File without changes
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <Entities total="0" results="0" page="1" page-size="25" href="https://s3-sandbox.parature.com/api/v1/5578/5633/Account" />
data/spec/hash_spec.rb ADDED
@@ -0,0 +1,49 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Hash, "to_xml_attributes" do
4
+ before do
5
+ @hash = { :one => "ONE", "two" => "TWO" }
6
+ end
7
+
8
+ it "should turn the hash into xml attributes" do
9
+ attrs = @hash.to_xml_attributes
10
+ attrs.should match(/one="ONE"/m)
11
+ attrs.should match(/two="TWO"/m)
12
+ end
13
+
14
+ it 'should preserve _ in hash keys' do
15
+ attrs = {
16
+ :some_long_attribute => "with short value",
17
+ :crash => :burn,
18
+ :merb => "uses extlib"
19
+ }.to_xml_attributes
20
+
21
+ attrs.should =~ /some_long_attribute="with short value"/
22
+ attrs.should =~ /merb="uses extlib"/
23
+ attrs.should =~ /crash="burn"/
24
+ end
25
+ end
26
+
27
+
28
+ describe Hash, 'to_params' do
29
+ {
30
+ { "foo" => "bar", "baz" => "bat" } => "foo=bar&baz=bat",
31
+ { "foo" => [ "bar", "baz" ] } => "foo[]=bar&foo[]=baz",
32
+ { "foo" => [ {"bar" => "1"}, {"bar" => 2} ] } => "foo[][bar]=1&foo[][bar]=2",
33
+ { "foo" => { "bar" => [ {"baz" => 1}, {"baz" => "2"} ] } } => "foo[bar][][baz]=1&foo[bar][][baz]=2",
34
+ { "foo" => {"1" => "bar", "2" => "baz"} } => "foo[1]=bar&foo[2]=baz"
35
+ }.each do |hash, params|
36
+ it "should covert hash: #{hash.inspect} to params: #{params.inspect}" do
37
+ hash.to_params.split('&').sort.should == params.split('&').sort
38
+ end
39
+ end
40
+
41
+ it 'should not leave a trailing &' do
42
+ { :name => 'Bob', :address => { :street => '111 Ruby Ave.', :city => 'Ruby Central', :phones => ['111-111-1111', '222-222-2222'] } }.to_params.should_not match(/&$/)
43
+ end
44
+
45
+ it 'should URL encode unsafe characters' do
46
+ {:q => "?&\" +"}.to_params.should == "q=%3F%26%22%20%2B"
47
+ end
48
+
49
+ end
@@ -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,42 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+
3
+ describe HTTParty::Parsers::JSON do
4
+ TESTS = {
5
+ %q({"data": "G\u00fcnter"}) => {"data" => "Günter"},
6
+ %q({"returnTo":{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}},
7
+ %q({returnTo:{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}},
8
+ %q({"return\\"To\\":":{"\/categories":"\/"}}) => {"return\"To\":" => {"/categories" => "/"}},
9
+ %q({"returnTo":{"\/categories":1}}) => {"returnTo" => {"/categories" => 1}},
10
+ %({"returnTo":[1,"a"]}) => {"returnTo" => [1, "a"]},
11
+ %({"returnTo":[1,"\\"a\\",", "b"]}) => {"returnTo" => [1, "\"a\",", "b"]},
12
+ %({a: "'", "b": "5,000"}) => {"a" => "'", "b" => "5,000"},
13
+ %({a: "a's, b's and c's", "b": "5,000"}) => {"a" => "a's, b's and c's", "b" => "5,000"},
14
+ %({a: "2007-01-01"}) => {'a' => Date.new(2007, 1, 1)},
15
+ %({a: "2007-01-01 01:12:34 Z"}) => {'a' => Time.utc(2007, 1, 1, 1, 12, 34)},
16
+ # no time zone
17
+ %({a: "2007-01-01 01:12:34"}) => {'a' => "2007-01-01 01:12:34"},
18
+ %([]) => [],
19
+ %({}) => {},
20
+ %(1) => 1,
21
+ %("") => "",
22
+ %("\\"") => "\"",
23
+ %(null) => nil,
24
+ %(true) => true,
25
+ %(false) => false,
26
+ %q("http:\/\/test.host\/posts\/1") => "http://test.host/posts/1"
27
+ }
28
+
29
+ TESTS.each do |json, expected|
30
+ it "should decode json (#{json})" do
31
+ lambda {
32
+ HTTParty::Parsers::JSON.decode(json).should == expected
33
+ }.should_not raise_error
34
+ end
35
+ end
36
+
37
+ it "should raise error for failed decoding" do
38
+ lambda {
39
+ HTTParty::Parsers::JSON.decode(%({: 1}))
40
+ }.should raise_error(HTTParty::Parsers::JSON::ParseError)
41
+ end
42
+ end