resourceful 0.5.4 → 0.6.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.
@@ -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
@@ -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'
@@ -10,9 +10,13 @@ require 'resourceful/util'
10
10
  require 'resourceful/header'
11
11
  require 'resourceful/http_accessor'
12
12
 
13
+ module Resourceful
14
+ autoload :MultipartFormData, 'resourceful/multipart_form_data'
15
+ autoload :UrlencodedFormData, 'resourceful/urlencoded_form_data'
16
+ end
17
+
13
18
  # Resourceful is a library that provides a high level HTTP interface.
14
19
  module Resourceful
15
- VERSION = "0.5.4"
20
+ VERSION = "0.6.0"
16
21
  RESOURCEFUL_USER_AGENT_TOKEN = "Resourceful/#{VERSION}(Ruby/#{RUBY_VERSION})"
17
-
18
22
  end
@@ -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