leh-httparty 0.3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/History +108 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Manifest +55 -0
  4. data/README +35 -0
  5. data/Rakefile +47 -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 +37 -0
  26. data/lib/core_extensions.rb +230 -0
  27. data/lib/httparty.rb +201 -0
  28. data/lib/httparty/cookie_hash.rb +9 -0
  29. data/lib/httparty/exceptions.rb +7 -0
  30. data/lib/httparty/module_inheritable_attributes.rb +25 -0
  31. data/lib/httparty/parsers.rb +4 -0
  32. data/lib/httparty/parsers/json.rb +74 -0
  33. data/lib/httparty/parsers/xml.rb +209 -0
  34. data/lib/httparty/request.rb +139 -0
  35. data/lib/httparty/response.rb +17 -0
  36. data/lib/httparty/version.rb +3 -0
  37. data/setup.rb +1585 -0
  38. data/spec/fixtures/delicious.xml +23 -0
  39. data/spec/fixtures/empty.xml +0 -0
  40. data/spec/fixtures/google.html +3 -0
  41. data/spec/fixtures/twitter.json +1 -0
  42. data/spec/fixtures/twitter.xml +403 -0
  43. data/spec/fixtures/undefined_method_add_node_for_nil.xml +2 -0
  44. data/spec/hash_spec.rb +49 -0
  45. data/spec/httparty/cookie_hash_spec.rb +38 -0
  46. data/spec/httparty/parsers/json_spec.rb +42 -0
  47. data/spec/httparty/parsers/xml_spec.rb +445 -0
  48. data/spec/httparty/request_spec.rb +196 -0
  49. data/spec/httparty/response_spec.rb +53 -0
  50. data/spec/httparty_spec.rb +259 -0
  51. data/spec/spec.opts +3 -0
  52. data/spec/spec_helper.rb +21 -0
  53. data/spec/string_spec.rb +27 -0
  54. data/website/css/common.css +47 -0
  55. data/website/index.html +74 -0
  56. metadata +133 -0
@@ -0,0 +1,201 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require 'net/http'
4
+ require 'net/https'
5
+ require 'core_extensions'
6
+ require 'httparty/module_inheritable_attributes'
7
+
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)
21
+
22
+ def self.included(base)
23
+ base.extend ClassMethods
24
+ base.send :include, HTTParty::ModuleInheritableAttributes
25
+ base.send(:mattr_inheritable, :default_options)
26
+ base.instance_variable_set("@default_options", {})
27
+ end
28
+
29
+ module ClassMethods
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
36
+ def http_proxy(addr=nil, port = nil)
37
+ default_options[:http_proxyaddr] = addr
38
+ default_options[:http_proxyport] = port
39
+ end
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
48
+ def base_uri(uri=nil)
49
+ return default_options[:base_uri] unless uri
50
+ default_options[:base_uri] = HTTParty.normalize_base_uri(uri)
51
+ end
52
+
53
+ # Allows setting basic authentication username and password.
54
+ #
55
+ # class Foo
56
+ # include HTTParty
57
+ # basic_auth 'username', 'password'
58
+ # end
59
+ def basic_auth(u, p)
60
+ default_options[:basic_auth] = {:username => u, :password => p}
61
+ end
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
70
+ def default_params(h={})
71
+ raise ArgumentError, 'Default params must be a hash' unless h.is_a?(Hash)
72
+ default_options[:default_params] ||= {}
73
+ default_options[:default_params].merge!(h)
74
+ end
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
82
+ def headers(h={})
83
+ raise ArgumentError, 'Headers must be a hash' unless h.is_a?(Hash)
84
+ default_options[:headers] ||= {}
85
+ default_options[:headers].merge!(h)
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
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
101
+ def format(f)
102
+ raise UnsupportedFormat, "Must be one of: #{AllowedFormats.values.join(', ')}" unless AllowedFormats.value?(f)
103
+ default_options[:format] = f
104
+ end
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})
118
+ def get(path, options={})
119
+ perform_request Net::HTTP::Get, path, options
120
+ end
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'})
134
+ def post(path, options={})
135
+ perform_request Net::HTTP::Post, path, options
136
+ end
137
+
138
+ def put(path, options={})
139
+ perform_request Net::HTTP::Put, path, options
140
+ end
141
+
142
+ def delete(path, options={})
143
+ perform_request Net::HTTP::Delete, path, options
144
+ end
145
+
146
+ def default_options #:nodoc:
147
+ @default_options
148
+ end
149
+
150
+ private
151
+ def perform_request(http_method, path, options) #:nodoc:
152
+ process_cookies(options)
153
+ Request.new(http_method, path, default_options.dup.merge(options)).perform
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
164
+ end
165
+
166
+ def self.normalize_base_uri(url) #:nodoc:
167
+ use_ssl = (url =~ /^https/) || url.include?(':443')
168
+ ends_with_slash = url =~ /\/$/
169
+
170
+ url.chop! if ends_with_slash
171
+ url.gsub!(/^https?:\/\//i, '')
172
+
173
+ "http#{'s' if use_ssl}://#{url}"
174
+ end
175
+
176
+ class Basement #:nodoc:
177
+ include HTTParty
178
+ end
179
+
180
+ def self.get(*args)
181
+ Basement.get(*args)
182
+ end
183
+
184
+ def self.post(*args)
185
+ Basement.post(*args)
186
+ end
187
+
188
+ def self.put(*args)
189
+ Basement.put(*args)
190
+ end
191
+
192
+ def self.delete(*args)
193
+ Basement.delete(*args)
194
+ end
195
+ end
196
+
197
+ require 'httparty/cookie_hash'
198
+ require 'httparty/exceptions'
199
+ require 'httparty/request'
200
+ require 'httparty/response'
201
+ require 'httparty/parsers'
@@ -0,0 +1,9 @@
1
+ class HTTParty::CookieHash < Hash #:nodoc:
2
+ def add_cookies(hash)
3
+ merge!(hash)
4
+ end
5
+
6
+ def to_cookie_string
7
+ collect { |k, v| "#{k}=#{v}" }.join("; ")
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module HTTParty
2
+ # Exception raised when you attempt to set a non-existant format
3
+ class UnsupportedFormat < StandardError; end
4
+
5
+ # Exception that is raised when request has redirected too many times
6
+ class RedirectionTooDeep < StandardError; end
7
+ end
@@ -0,0 +1,25 @@
1
+ module HTTParty
2
+ module ModuleInheritableAttributes #:nodoc:
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods #:nodoc:
8
+ def mattr_inheritable(*args)
9
+ @mattr_inheritable_attrs ||= [:mattr_inheritable_attrs]
10
+ @mattr_inheritable_attrs += args
11
+ args.each do |arg|
12
+ module_eval %(class << self; attr_accessor :#{arg} end)
13
+ end
14
+ @mattr_inheritable_attrs
15
+ end
16
+
17
+ def inherited(subclass)
18
+ @mattr_inheritable_attrs.each do |inheritable_attribute|
19
+ instance_var = "@#{inheritable_attribute}"
20
+ subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ Dir[File.dirname(__FILE__) + "/parsers/*.rb"].sort.each do |path|
2
+ filename = File.basename(path)
3
+ require "httparty/parsers/#{filename}"
4
+ end
@@ -0,0 +1,74 @@
1
+ # Copyright (c) 2004-2008 David Heinemeier Hansson
2
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
3
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
4
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
5
+
6
+ require 'yaml'
7
+ require 'strscan'
8
+
9
+ module HTTParty
10
+ module Parsers #:nodoc:
11
+ module JSON #:nodoc:
12
+ class ParseError < StandardError #:nodoc:
13
+ end
14
+
15
+ def self.decode(json)
16
+ YAML.load(unescape(convert_json_to_yaml(json)))
17
+ rescue ArgumentError => e
18
+ raise ParseError, "Invalid JSON string"
19
+ end
20
+
21
+ protected
22
+
23
+ def self.unescape(str)
24
+ str.gsub(/\\u([0-9a-f]{4})/) {
25
+ [$1.hex].pack("U")
26
+ }
27
+ end
28
+
29
+ # matches YAML-formatted dates
30
+ DATE_REGEX = /^\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[ \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?$/
31
+
32
+ # Ensure that ":" and "," are always followed by a space
33
+ def self.convert_json_to_yaml(json) #:nodoc:
34
+ scanner, quoting, marks, pos, times = StringScanner.new(json), false, [], nil, []
35
+ while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/)
36
+ case char = scanner[1]
37
+ when '"', "'"
38
+ if !quoting
39
+ quoting = char
40
+ pos = scanner.pos
41
+ elsif quoting == char
42
+ if json[pos..scanner.pos-2] =~ DATE_REGEX
43
+ # found a date, track the exact positions of the quotes so we can remove them later.
44
+ # oh, and increment them for each current mark, each one is an extra padded space that bumps
45
+ # the position in the final YAML output
46
+ total_marks = marks.size
47
+ times << pos+total_marks << scanner.pos+total_marks
48
+ end
49
+ quoting = false
50
+ end
51
+ when ":",","
52
+ marks << scanner.pos - 1 unless quoting
53
+ end
54
+ end
55
+
56
+ if marks.empty?
57
+ json.gsub(/\\\//, '/')
58
+ else
59
+ left_pos = [-1].push(*marks)
60
+ right_pos = marks << json.length
61
+ output = []
62
+ left_pos.each_with_index do |left, i|
63
+ output << json[left.succ..right_pos[i]]
64
+ end
65
+ output = output * " "
66
+
67
+ times.each { |i| output[i-1] = ' ' }
68
+ output.gsub!(/\\\//, '/')
69
+ output
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,209 @@
1
+ require 'rexml/parsers/streamparser'
2
+ require 'rexml/parsers/baseparser'
3
+ require 'rexml/light/node'
4
+
5
+ # This is a slighly modified version of the XMLUtilityNode from
6
+ # http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
7
+ # It's mainly just adding vowels, as I ht cd wth n vwls :)
8
+ # This represents the hard part of the work, all I did was change the
9
+ # underlying parser.
10
+ class REXMLUtilityNode #:nodoc:
11
+ attr_accessor :name, :attributes, :children, :type
12
+
13
+ def self.typecasts
14
+ @@typecasts
15
+ end
16
+
17
+ def self.typecasts=(obj)
18
+ @@typecasts = obj
19
+ end
20
+
21
+ def self.available_typecasts
22
+ @@available_typecasts
23
+ end
24
+
25
+ def self.available_typecasts=(obj)
26
+ @@available_typecasts = obj
27
+ end
28
+
29
+ self.typecasts = {}
30
+ self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
31
+ self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
32
+ self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
33
+ self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
34
+ self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
35
+ self.typecasts["decimal"] = lambda{|v| v.nil? ? nil : BigDecimal(v.to_s)}
36
+ self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
37
+ self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
38
+ self.typecasts["symbol"] = lambda{|v| v.nil? ? nil : v.to_sym}
39
+ self.typecasts["string"] = lambda{|v| v.to_s}
40
+ self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
41
+ self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
42
+
43
+ self.available_typecasts = self.typecasts.keys
44
+
45
+ def initialize(name, attributes = {})
46
+ @name = name.tr("-", "_")
47
+ # leave the type alone if we don't know what it is
48
+ @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
49
+
50
+ @nil_element = attributes.delete("nil") == "true"
51
+ @attributes = undasherize_keys(attributes)
52
+ @children = []
53
+ @text = false
54
+ end
55
+
56
+ def add_node(node)
57
+ @text = true if node.is_a? String
58
+ @children << node
59
+ end
60
+
61
+ def to_hash
62
+ if @type == "file"
63
+ f = StringIO.new((@children.first || '').unpack('m').first)
64
+ class << f
65
+ attr_accessor :original_filename, :content_type
66
+ end
67
+ f.original_filename = attributes['name'] || 'untitled'
68
+ f.content_type = attributes['content_type'] || 'application/octet-stream'
69
+ return {name => f}
70
+ end
71
+
72
+ if @text
73
+ return { name => typecast_value( translate_xml_entities( inner_html ) ) }
74
+ else
75
+ #change repeating groups into an array
76
+ groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
77
+
78
+ out = nil
79
+ if @type == "array"
80
+ out = []
81
+ groups.each do |k, v|
82
+ if v.size == 1
83
+ out << v.first.to_hash.entries.first.last
84
+ else
85
+ out << v.map{|e| e.to_hash[k]}
86
+ end
87
+ end
88
+ out = out.flatten
89
+
90
+ else # If Hash
91
+ out = {}
92
+ groups.each do |k,v|
93
+ if v.size == 1
94
+ out.merge!(v.first)
95
+ else
96
+ out.merge!( k => v.map{|e| e.to_hash[k]})
97
+ end
98
+ end
99
+ out.merge! attributes unless attributes.empty?
100
+ out = out.empty? ? nil : out
101
+ end
102
+
103
+ if @type && out.nil?
104
+ { name => typecast_value(out) }
105
+ else
106
+ { name => out }
107
+ end
108
+ end
109
+ end
110
+
111
+ # Typecasts a value based upon its type. For instance, if
112
+ # +node+ has #type == "integer",
113
+ # {{[node.typecast_value("12") #=> 12]}}
114
+ #
115
+ # @param value<String> The value that is being typecast.
116
+ #
117
+ # @details [:type options]
118
+ # "integer"::
119
+ # converts +value+ to an integer with #to_i
120
+ # "boolean"::
121
+ # checks whether +value+, after removing spaces, is the literal
122
+ # "true"
123
+ # "datetime"::
124
+ # Parses +value+ using Time.parse, and returns a UTC Time
125
+ # "date"::
126
+ # Parses +value+ using Date.parse
127
+ #
128
+ # @return <Integer, TrueClass, FalseClass, Time, Date, Object>
129
+ # The result of typecasting +value+.
130
+ #
131
+ # @note
132
+ # If +self+ does not have a "type" key, or if it's not one of the
133
+ # options specified above, the raw +value+ will be returned.
134
+ def typecast_value(value)
135
+ return value unless @type
136
+ proc = self.class.typecasts[@type]
137
+ proc.nil? ? value : proc.call(value)
138
+ end
139
+
140
+ # Convert basic XML entities into their literal values.
141
+ #
142
+ # @param value<#gsub> An XML fragment.
143
+ #
144
+ # @return <#gsub> The XML fragment after converting entities.
145
+ def translate_xml_entities(value)
146
+ value.gsub(/&lt;/, "<").
147
+ gsub(/&gt;/, ">").
148
+ gsub(/&quot;/, '"').
149
+ gsub(/&apos;/, "'").
150
+ gsub(/&amp;/, "&")
151
+ end
152
+
153
+ # Take keys of the form foo-bar and convert them to foo_bar
154
+ def undasherize_keys(params)
155
+ params.keys.each do |key, value|
156
+ params[key.tr("-", "_")] = params.delete(key)
157
+ end
158
+ params
159
+ end
160
+
161
+ # Get the inner_html of the REXML node.
162
+ def inner_html
163
+ @children.join
164
+ end
165
+
166
+ # Converts the node into a readable HTML node.
167
+ #
168
+ # @return <String> The HTML node in text form.
169
+ def to_html
170
+ attributes.merge!(:type => @type ) if @type
171
+ "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
172
+ end
173
+
174
+ # @alias #to_html #to_s
175
+ def to_s
176
+ to_html
177
+ end
178
+ end
179
+
180
+ module HTTParty
181
+ module Parsers #:nodoc:
182
+ module XML #:nodoc:
183
+ def self.parse(xml)
184
+ stack = []
185
+ parser = REXML::Parsers::BaseParser.new(xml)
186
+
187
+ while true
188
+ event = parser.pull
189
+ case event[0]
190
+ when :end_document
191
+ break
192
+ when :end_doctype, :start_doctype
193
+ # do nothing
194
+ when :start_element
195
+ stack.push REXMLUtilityNode.new(event[1], event[2])
196
+ when :end_element
197
+ if stack.size > 1
198
+ temp = stack.pop
199
+ stack.last.add_node(temp)
200
+ end
201
+ when :text, :cdata
202
+ stack.last.add_node(event[1]) unless event[1].strip.length == 0 || stack.empty?
203
+ end
204
+ end
205
+ stack.pop.to_hash
206
+ end
207
+ end
208
+ end
209
+ end