paul-resourceful 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,18 @@
1
+ Version 0.6.0
2
+ =============
3
+
4
+ * Improved support for multi-valued header fields. (Peter Williams)
5
+ * Added convenience mechanisms for making URL encoded and multipart form data requests. (Peter Williams)
6
+ * Ruby 1.9 support (Peter Williams)
7
+
8
+ Compatibility issues
9
+ --------------------
10
+
11
+ * The semantics of the Resourceful::Header API have changed slightly.
12
+ Previously, any header might return a string or an array. Now
13
+ fields are either "single-value" and always return a string, or
14
+ "multi-value" and always return an array. See API doc for more
15
+ details.
16
+
17
+
18
+
data/Manifest CHANGED
@@ -1,17 +1,22 @@
1
1
  lib/resourceful.rb
2
2
  lib/resourceful/net_http_adapter.rb
3
+ lib/resourceful/options_interpretation.rb
3
4
  lib/resourceful/stubbed_resource_proxy.rb
5
+ lib/resourceful/urlencoded_form_data.rb
4
6
  lib/resourceful/header.rb
5
7
  lib/resourceful/memcache_cache_manager.rb
6
8
  lib/resourceful/response.rb
7
9
  lib/resourceful/util.rb
8
- lib/resourceful/options_interpreter.rb
10
+ lib/resourceful/abstract_form_data.rb
9
11
  lib/resourceful/cache_manager.rb
10
12
  lib/resourceful/request.rb
11
13
  lib/resourceful/resource.rb
12
14
  lib/resourceful/exceptions.rb
15
+ lib/resourceful/multipart_form_data.rb
13
16
  lib/resourceful/http_accessor.rb
14
17
  lib/resourceful/authentication_manager.rb
18
+ History.txt
19
+ resourceful.gemspec
15
20
  README.markdown
16
21
  MIT-LICENSE
17
22
  Rakefile
@@ -26,4 +31,9 @@ spec/acceptance/header_spec.rb
26
31
  spec/acceptance/resource_spec.rb
27
32
  spec/acceptance/caching_spec.rb
28
33
  spec/acceptance/redirecting_spec.rb
34
+ spec/resourceful/multipart_form_data_spec.rb
35
+ spec/resourceful/header_spec.rb
36
+ spec/resourceful/resource_spec.rb
37
+ spec/resourceful/urlencoded_form_data_spec.rb
38
+ spec/caching_spec.rb
29
39
  spec/spec.opts
data/README.markdown CHANGED
@@ -58,8 +58,17 @@ Post a URL encoded form
58
58
 
59
59
  require 'resourceful'
60
60
  http = Resourceful::HttpAccessor.new
61
- resp = http.resource('http://mysite.example/service').
62
- post('hostname=test&level=super', :content_type => 'application/x-www-form-urlencoded')
61
+ resp = http.resource('http://mysite.example/service').
62
+ post(Resourceful::UrlencodedFormData.new(:hostname => 'test', :level => 'super'))
63
+
64
+ Post a Mulitpart form with a file
65
+ -----------------------
66
+
67
+ require 'resourceful'
68
+ http = Resourceful::HttpAccessor.new
69
+ form_data = Resourceful::MultipartFormData.new(:username => 'me')
70
+ form_data.add_file('avatar', '/tmp/my_avatar.png', 'image/png')
71
+ resp = http.resource('http://mysite.example/service').post(form_data)
63
72
 
64
73
  Put an XML document
65
74
  -------------------
data/Rakefile CHANGED
@@ -12,8 +12,9 @@ begin
12
12
  p.email = "psadauskas@gmail.com"
13
13
 
14
14
  p.ignore_pattern = ["pkg/*", "tmp/*"]
15
- p.dependencies = ['addressable', 'httpauth']
15
+ p.dependencies = [['addressable', '>= 2.1.0'], 'httpauth']
16
16
  p.development_dependencies = ['thin', 'yard', 'sinatra', 'rspec']
17
+ p.retain_gemspec = true
17
18
  end
18
19
  rescue LoadError => e
19
20
  puts "install 'echoe' gem to be able to build the gem"
@@ -21,31 +22,50 @@ end
21
22
 
22
23
  require 'spec/rake/spectask'
23
24
 
24
- desc 'Run all specs'
25
+ desc 'Run all acceptance specs'
26
+
25
27
  Spec::Rake::SpecTask.new(:spec) do |t|
26
28
  t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
27
29
  t.libs << 'lib'
28
- t.spec_files = FileList['spec/acceptance/*_spec.rb']
30
+ t.spec_files = FileList['spec/**/*_spec.rb']
29
31
  end
30
32
 
31
- desc 'Run the specs for the server'
32
- Spec::Rake::SpecTask.new('spec:server') do |t|
33
- t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
34
- t.libs << 'lib'
35
- t.spec_files = FileList['spec/simple_sinatra_server_spec.rb']
33
+ namespace :spec do
34
+ desc 'Run all acceptance specs'
35
+ Spec::Rake::SpecTask.new(:acceptance) do |t|
36
+ t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
37
+ t.libs << 'lib'
38
+ t.spec_files = FileList['spec/acceptance/*_spec.rb']
39
+ end
40
+
41
+ desc 'Run all unit specs'
42
+ Spec::Rake::SpecTask.new(:unit) do |t|
43
+ t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
44
+ t.libs << 'lib'
45
+ t.spec_files = FileList['spec/**/*_spec.rb'] - (FileList['spec/acceptance/*_spec.rb'] + FileList['spec/simple_sinatra_server_spec.rb'] )
46
+ end
47
+
48
+ desc 'Run the specs for the server'
49
+ Spec::Rake::SpecTask.new('server') do |t|
50
+ t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
51
+ t.libs << 'lib'
52
+ t.spec_files = FileList['spec/simple_sinatra_server_spec.rb']
53
+ end
36
54
  end
37
55
 
38
- begin
39
- require 'spec/simple_sinatra_server'
40
- desc "Run the sinatra echo server, with loggin"
41
- task :server do
42
- Sinatra::Default.set(
43
- :run => true,
44
- :logging => true
45
- )
56
+ task :server do
57
+ begin
58
+ require 'spec/simple_sinatra_server'
59
+ desc "Run the sinatra echo server, with loggin"
60
+ task :server do
61
+ Sinatra::Default.set(
62
+ :run => true,
63
+ :logging => true
64
+ )
65
+ end
66
+ rescue LoadError => e
67
+ puts "Install 'sinatra' gem to run the server"
46
68
  end
47
- rescue LoadError => e
48
- puts "Install 'sinatra' gem to run the server"
49
69
  end
50
70
 
51
71
  desc 'Default: Run Specs'
@@ -0,0 +1,18 @@
1
+ module Resourceful
2
+ class AbstractFormData
3
+ def initialize(contents = {})
4
+ @form_data = []
5
+
6
+ contents.each do |k,v|
7
+ add(k, v)
8
+ end
9
+ end
10
+
11
+ def add(name, value)
12
+ form_data << [name.to_s, value]
13
+ end
14
+
15
+ protected
16
+ attr_reader :form_data
17
+ end
18
+ end
@@ -1,125 +1,256 @@
1
- # A case-normalizing Hash, adjusting on [] and []=.
2
- # Shamelessly swiped from Rack
1
+ require 'resourceful/options_interpretation'
2
+ require 'set'
3
+
4
+ # Represents the header fields of an HTTP message. To access a field
5
+ # you can use `#[]` and `#[]=`. For example, to get the content type
6
+ # of a response you can do
7
+ #
8
+ # response.header['Content-Type'] # => "application/xml"
9
+ #
10
+ # Lookups and modifications done in this way are case insensitive, so
11
+ # 'Content-Type', 'content-type' and :content_type are all equivalent.
12
+ #
13
+ # Multi-valued fields
14
+ # -------------------
15
+ #
16
+ # Multi-value fields (e.g. Accept) are always returned as an Array
17
+ # regardless of the number of values, if the field is present.
18
+ # Single-value fields (e.g. Content-Type) are always returned as
19
+ # strings. The multi/single valueness of a header field is determined
20
+ # by the way it is defined in the HTTP spec. Unknown fields are
21
+ # treated as multi-valued.
22
+ #
23
+ # (This behavior is new in 0.6 and may be slightly incompatible with
24
+ # the way previous versions worked in some situations.)
25
+ #
26
+ # For example
27
+ #
28
+ # h = Resourceful::Header.new
29
+ # h['Accept'] = "application/xml"
30
+ # h['Accept'] # => ["application/xml"]
31
+ #
3
32
  module Resourceful
4
- class Header < Hash
33
+ class Header
34
+ include Enumerable
35
+
5
36
  def initialize(hash={})
37
+ @raw_fields = {}
6
38
  hash.each { |k, v| self[k] = v }
7
39
  end
8
40
 
9
41
  def to_hash
10
- {}.replace(self)
42
+ @raw_fields.dup
11
43
  end
12
44
 
13
45
  def [](k)
14
- super capitalize(k)
46
+ field_def(k).get_from(@raw_fields)
15
47
  end
16
48
 
17
49
  def []=(k, v)
18
- super capitalize(k), v
50
+ field_def(k).set_to(v, @raw_fields)
19
51
  end
20
52
 
21
53
  def has_key?(k)
22
- super capitalize(k)
54
+ field_def(k).exists_in?(@raw_fields)
55
+ end
56
+
57
+ def each(&blk)
58
+ @raw_fields.each(&blk)
59
+ end
60
+ alias each_field each
61
+
62
+ def merge!(another)
63
+ another.each do |k,v|
64
+ self[k] = v
65
+ end
66
+ self
23
67
  end
24
68
 
25
- def capitalize(k)
26
- k.to_s.downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }.gsub('_', '-')
69
+ def merge(another)
70
+ self.class.new(self).merge!(another)
27
71
  end
28
72
 
29
- def each_field(&blk)
30
- to_hash.each { |k,v|
31
- blk.call capitalize(k), v
32
- }
73
+ def reverse_merge(another)
74
+ self.class.new(another).merge!(self)
33
75
  end
34
76
 
35
- HEADERS = %w[
36
- Accept
37
- Accept-Charset
38
- Accept-Encoding
39
- Accept-Language
40
- Accept-Ranges
41
- Age
42
- Allow
43
- Authorization
44
- Cache-Control
45
- Connection
46
- Content-Encoding
47
- Content-Language
48
- Content-Length
49
- Content-Location
50
- Content-MD5
51
- Content-Range
52
- Content-Type
53
- Date
54
- ETag
55
- Expect
56
- Expires
57
- From
58
- Host
59
- If-Match
60
- If-Modified-Since
61
- If-None-Match
62
- If-Range
63
- If-Unmodified-Since
64
- Keep-Alive
65
- Last-Modified
66
- Location
67
- Max-Forwards
68
- Pragma
69
- Proxy-Authenticate
70
- Proxy-Authorization
71
- Range
72
- Referer
73
- Retry-After
74
- Server
75
- TE
76
- Trailer
77
- Transfer-Encoding
78
- Upgrade
79
- User-Agent
80
- Vary
81
- Via
82
- Warning
83
- WWW-Authenticate
84
- ]
85
-
86
- HEADERS.each do |header|
87
- const = header.upcase.gsub('-', '_')
88
- meth = header.downcase.gsub('-', '_')
89
-
90
- class_eval <<-RUBY, __FILE__, __LINE__
91
- #{const} = "#{header}".freeze # ACCEPT = "accept".freeze
92
-
93
- def #{meth} # def accept
94
- self[#{const}] # self[ACCEPT]
95
- end # end
96
-
97
- def #{meth}=(str) # def accept=(str)
98
- self[#{const}] = str # self[ACCEPT] = str
99
- end # end
100
- RUBY
77
+ def dup
78
+ self.class.new(@raw_fields.dup)
79
+ end
80
+
81
+
82
+ # Class to handle the details of each type of field.
83
+ class HeaderFieldDef
84
+ include Comparable
85
+ include OptionsInterpretation
86
+
87
+ ##
88
+ attr_reader :name
89
+
90
+ def initialize(name, options = {})
91
+ @name = name
92
+ extract_opts(options) do |opts|
93
+ @repeatable = opts.extract(:repeatable, :default => false)
94
+ @hop_by_hop = opts.extract(:hop_by_hop, :default => false)
95
+ @modifiable = opts.extract(:modifiable, :default => true)
96
+ end
97
+ end
98
+
99
+ def repeatable?
100
+ @repeatable
101
+ end
102
+
103
+ def hop_by_hop?
104
+ @hop_by_hop
105
+ end
106
+
107
+ def modifiable?
108
+ @modifiable
109
+ end
110
+
111
+ def get_from(raw_fields_hash)
112
+ raw_fields_hash[name]
113
+ end
114
+
115
+ def set_to(value, raw_fields_hash)
116
+ raw_fields_hash[name] = if repeatable?
117
+ Array(value)
118
+ elsif value.kind_of?(Array)
119
+ raise ArgumentError, "#{name} field may only have one value" if value.size > 1
120
+ value.first
121
+ else
122
+ value
123
+ end
124
+ end
125
+
126
+ def exists_in?(raw_fields_hash)
127
+ raw_fields_hash.has_key?(name)
128
+ end
129
+
130
+ def <=>(another)
131
+ name <=> another.name
132
+ end
101
133
 
134
+ def ==(another)
135
+ name_pattern === another.name
136
+ end
137
+ alias eql? ==
138
+
139
+ def ===(another)
140
+ if another.kind_of?(HeaderFieldDef)
141
+ self == another
142
+ else
143
+ name_pattern === another
144
+ end
145
+ end
146
+
147
+ def name_pattern
148
+ Regexp.new('^' + name.gsub('-', '[_-]') + '$', Regexp::IGNORECASE)
149
+ end
150
+
151
+ def methodized_name
152
+ name.downcase.gsub('-', '_')
153
+ end
154
+
155
+ alias to_s name
156
+
157
+ def gen_setter(klass)
158
+ klass.class_eval <<-RUBY
159
+ def #{methodized_name}=(val) # def accept=(val)
160
+ self['#{name}'] = val # self['Accept'] = val
161
+ end # end
162
+ RUBY
163
+ end
164
+
165
+ def gen_getter(klass)
166
+ klass.class_eval <<-RUBY
167
+ def #{methodized_name} # def accept
168
+ self['#{name}'] # self['Accept']
169
+ end # end
170
+ RUBY
171
+ end
172
+
173
+ def gen_canonical_name_const(klass)
174
+ const_name = name.upcase.gsub('-', '_')
175
+
176
+ klass.const_set(const_name, name)
177
+ end
178
+ end
179
+
180
+ @@header_field_defs = Set.new
181
+
182
+ def self.header_field(name, options = {})
183
+ hfd = HeaderFieldDef.new(name, options)
184
+
185
+ @@header_field_defs << hfd
186
+
187
+ hfd.gen_getter(self)
188
+ hfd.gen_setter(self)
189
+ hfd.gen_canonical_name_const(self)
190
+ end
191
+
192
+ def self.hop_by_hop_headers
193
+ @@header_field_defs.select{|hfd| hfd.hop_by_hop?}
194
+ end
195
+
196
+ def self.non_modifiable_headers
197
+ @@header_field_defs.reject{|hfd| hfd.repeatable?}
198
+ end
199
+
200
+ def field_def(name)
201
+ @@header_field_defs.find{|hfd| hfd === name} ||
202
+ HeaderFieldDef.new(name.to_s.downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }.gsub('_', '-'), :repeatable => true)
102
203
  end
103
204
 
104
- HOP_BY_HOP_HEADERS = [
105
- CONNECTION,
106
- KEEP_ALIVE,
107
- PROXY_AUTHENTICATE,
108
- PROXY_AUTHORIZATION,
109
- TE,
110
- TRAILER,
111
- TRANSFER_ENCODING,
112
- UPGRADE
113
- ].freeze
114
-
115
- NON_MODIFIABLE_HEADERS = [
116
- CONTENT_LOCATION,
117
- CONTENT_MD5,
118
- ETAG,
119
- LAST_MODIFIED,
120
- EXPIRES
121
- ].freeze
122
205
 
206
+ header_field('Accept', :repeatable => true)
207
+ header_field('Accept-Charset', :repeatable => true)
208
+ header_field('Accept-Encoding', :repeatable => true)
209
+ header_field('Accept-Language', :repeatable => true)
210
+ header_field('Accept-Ranges', :repeatable => true)
211
+ header_field('Age')
212
+ header_field('Allow', :repeatable => true)
213
+ header_field('Authorization', :repeatable => true)
214
+ header_field('Cache-Control', :repeatable => true)
215
+ header_field('Connection', :hop_by_hop => true)
216
+ header_field('Content-Encoding', :repeatable => true)
217
+ header_field('Content-Language', :repeatable => true)
218
+ header_field('Content-Length')
219
+ header_field('Content-Location', :modifiable => false)
220
+ header_field('Content-MD5', :modifiable => false)
221
+ header_field('Content-Range')
222
+ header_field('Content-Type')
223
+ header_field('Date')
224
+ header_field('ETag', :modifiable => false)
225
+ header_field('Expect', :repeatable => true)
226
+ header_field('Expires', :modifiable => false)
227
+ header_field('From')
228
+ header_field('Host')
229
+ header_field('If-Match', :repeatable => true)
230
+ header_field('If-Modified-Since')
231
+ header_field('If-None-Match', :repeatable => true)
232
+ header_field('If-Range')
233
+ header_field('If-Unmodified-Since')
234
+ header_field('Keep-Alive', :hop_by_hop => true)
235
+ header_field('Last-Modified', :modifiable => false)
236
+ header_field('Location')
237
+ header_field('Max-Forwards')
238
+ header_field('Pragma', :repeatable => true)
239
+ header_field('Proxy-Authenticate', :hop_by_hop => true)
240
+ header_field('Proxy-Authorization', :hop_by_hop => true)
241
+ header_field('Range')
242
+ header_field('Referer')
243
+ header_field('Retry-After')
244
+ header_field('Server')
245
+ header_field('TE', :repeatable => true, :hop_by_hop => true)
246
+ header_field('Trailer', :repeatable => true, :hop_by_hop => true)
247
+ header_field('Transfer-Encoding', :repeatable => true, :hop_by_hop => true)
248
+ header_field('Upgrade', :repeatable => true, :hop_by_hop => true)
249
+ header_field('User-Agent')
250
+ header_field('Vary', :repeatable => true)
251
+ header_field('Via', :repeatable => true)
252
+ header_field('Warning', :repeatable => true)
253
+ header_field('WWW-Authenticate', :repeatable => true)
123
254
  end
124
255
  end
125
256
 
@@ -1,6 +1,6 @@
1
1
  require 'net/http'
2
2
 
3
- require 'resourceful/options_interpreter'
3
+ require 'resourceful/options_interpretation'
4
4
  require 'resourceful/authentication_manager'
5
5
  require 'resourceful/cache_manager'
6
6
  require 'resourceful/resource'
@@ -28,7 +28,8 @@ module Resourceful
28
28
  # provided by the Resourceful library. Conceptually this object
29
29
  # acts a collection of all the resources available via HTTP.
30
30
  class HttpAccessor
31
-
31
+ include OptionsInterpretation
32
+
32
33
  # A logger object to which messages about the activities of this
33
34
  # object will be written. This should be an object that responds
34
35
  # to +#info(message)+ and +#debug(message)+.
@@ -40,40 +41,45 @@ module Resourceful
40
41
  attr_reader :auth_manager
41
42
  attr_reader :user_agent_tokens
42
43
 
43
- INIT_OPTIONS = OptionsInterpreter.new do
44
- option(:logger, :default => Resourceful::BitBucketLogger.new)
45
- option(:user_agent, :default => []) {|ua| [ua].flatten}
46
- option(:cache_manager, :default => NullCacheManager.new)
47
- option(:authenticator)
48
- option(:authenticators, :default => [])
49
- end
44
+ ##
45
+ # The adapter this accessor will use to make the actual HTTP requests.
46
+ attr_reader :http_adapter
50
47
 
51
48
  # Initializes a new HttpAccessor. Valid options:
52
49
  #
53
- # +:logger+:: A Logger object that the new HTTP accessor should
54
- # send log messages
50
+ # `:logger`
51
+ # : A Logger object that the new HTTP accessor should send log messages
52
+ #
53
+ # `:user_agent`
54
+ # : One or more additional user agent tokens to added to the user agent string.
55
55
  #
56
- # +:user_agent+:: One or more additional user agent tokens to
57
- # added to the user agent string.
56
+ # `:cache_manager`
57
+ # : The cache manager this accessor should use.
58
58
  #
59
- # +:cache_manager+:: The cache manager this accessor should use.
59
+ # `:authenticator`
60
+ # : Add a single authenticator for this accessor.
60
61
  #
61
- # +:authenticator+:: Add a single authenticator for this accessor.
62
+ # `:authenticators`
63
+ # : Enumerable of the authenticators for this accessor.
62
64
  #
63
- # +:authenticators+:: Enumerable of the authenticators for this
64
- # accessor.
65
+ # `http_adapter`
66
+ # : The HttpAdapter to be used by this accessor
67
+ #
68
+ #
65
69
  def initialize(options = {})
66
70
  @user_agent_tokens = [RESOURCEFUL_USER_AGENT_TOKEN]
67
-
68
- INIT_OPTIONS.interpret(options) do |opts|
69
- @user_agent_tokens.push(*opts[:user_agent].reverse)
70
- self.logger = opts[:logger]
71
- @auth_manager = AuthenticationManager.new()
72
- @cache_manager = opts[:cache_manager]
71
+ @auth_manager = AuthenticationManager.new()
72
+
73
+ extract_opts(options) do |opts|
74
+ @user_agent_tokens.push(*opts.extract(:user_agent, :default => []) {|ua| [ua].flatten})
75
+
76
+ self.logger = opts.extract(:logger, :default => BitBucketLogger.new)
77
+ @cache_manager = opts.extract(:cache_manager, :default => NullCacheManager.new)
78
+ @http_adapter = opts.extract(:http_adapter, :default => lambda{NetHttpAdapter.new})
73
79
 
74
- add_authenticator(opts[:authenticator]) if opts[:authenticator]
75
- opts[:authenticators].each { |a| add_authenticator(a) }
76
- end
80
+ opts.extract(:authenticator, :required => false).tap{|a| add_authenticator(a) if a}
81
+ opts.extract(:authenticators, :default => []).each { |a| add_authenticator(a) }
82
+ end
77
83
  end
78
84
 
79
85
  # Returns the string that identifies this HTTP accessor. If you
@@ -0,0 +1,46 @@
1
+ require 'resourceful/abstract_form_data'
2
+
3
+ module Resourceful
4
+ class MultipartFormData < AbstractFormData
5
+ FileParamValue = Struct.new(:content, :file_name, :content_type)
6
+
7
+ def add_file(name, file_name, content_type="application/octet-stream")
8
+ add(name, FileParamValue.new(File.new(file_name, 'r'), File.basename(file_name), content_type))
9
+ end
10
+
11
+ def content_type
12
+ "multipart/form-data; boundary=#{boundary}"
13
+ end
14
+
15
+ def read
16
+ StringIO.new.tap do |out|
17
+ first = true
18
+ form_data.each do |key, val|
19
+ out << "\r\n" unless first
20
+ out << "--" << boundary
21
+ out << "\r\nContent-Disposition: form-data; name=\"#{key}\""
22
+ if val.kind_of?(FileParamValue)
23
+ out << "; filename=\"#{val.file_name}\""
24
+ out << "\r\nContent-Type: #{val.content_type}"
25
+ end
26
+ out << "\r\n\r\n"
27
+ if val.kind_of?(FileParamValue)
28
+ out << val.content.read
29
+ else
30
+ out << val.to_s
31
+ end
32
+ first = false
33
+ end
34
+ out << "\r\n--#{boundary}--"
35
+ end.string
36
+ end
37
+
38
+ protected
39
+
40
+ def boundary
41
+ @boundary ||= (0..30).map{BOUNDARY_CHARS[rand(BOUNDARY_CHARS.length)]}.join
42
+ end
43
+
44
+ BOUNDARY_CHARS = [('a'..'z').to_a,('A'..'Z').to_a,(0..9).to_a].flatten
45
+ end
46
+ end