rackful 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,288 @@
1
+ # Required for parsing:
2
+ require 'rackful_resource.rb'
3
+ require 'rackful_serializer.rb'
4
+
5
+ # Required for running
6
+ require 'rexml/rexml'
7
+
8
+
9
+ module Rackful
10
+
11
+ =begin markdown
12
+ Exception which represents an HTTP Status response.
13
+ @since 0.1.0
14
+ @abstract
15
+ =end
16
+ class HTTPStatus < RuntimeError
17
+
18
+
19
+ include Resource
20
+ attr_reader :status, :headers, :to_rackful
21
+
22
+
23
+ =begin markdown
24
+ @param message [String] XHTML
25
+ @param status [Symbol, Integer] e.g. `404` or `:not_found`
26
+ @param headers [Hash] HTTP response headers
27
+ =end
28
+ def initialize status, message = nil, info = {}
29
+ self.path = Request.current.path
30
+ @status = status_code status
31
+ raise "Wrong status: #{status}" if 0 == @status
32
+ message ||= ''
33
+ @headers = {}
34
+ @to_rackful = {}
35
+ info.each do
36
+ |k, v|
37
+ if k.kind_of? Symbol then @to_rackful[k] = v
38
+ else @headers[k] = v end
39
+ end
40
+ @to_rackful = nil if @to_rackful.empty?
41
+ begin
42
+ REXML::Document.new \
43
+ '<?xml version="1.0" encoding="UTF-8" ?>' +
44
+ "<div>#{message}</div>"
45
+ rescue
46
+ message = Rack::Utils.escape_html(message)
47
+ end
48
+ super message
49
+ if 500 <= @status
50
+ errors = Request.current.env['rack.errors']
51
+ errors.puts self.inspect
52
+ errors.puts "Headers: #{@headers.inspect}"
53
+ errors.puts "Info: #{@to_rackful.inspect}"
54
+ end
55
+ end
56
+
57
+
58
+ class XHTML < ::Rackful::XHTML
59
+
60
+
61
+ HTTP_STATUS_CODES = Rack::Utils::HTTP_STATUS_CODES
62
+
63
+
64
+ def header
65
+ <<EOS
66
+ <title>#{self.resource.status} #{HTTP_STATUS_CODES[self.resource.status]}</title>
67
+ </head><body>
68
+ <h1>HTTP/1.1 #{self.resource.status} #{HTTP_STATUS_CODES[self.resource.status]}</h1>
69
+ <div id="rackful_description">#{self.resource.message}</div>
70
+ EOS
71
+ end
72
+
73
+
74
+ def headers
75
+ self.resource.headers
76
+ end
77
+
78
+
79
+ end # class HTTPStatus::XHTML
80
+
81
+
82
+ add_serializer XHTML, 1.0
83
+
84
+
85
+ end # class Rackful::HTTPStatus
86
+
87
+
88
+ =begin markdown
89
+ @abstract Base class for HTTP status codes with only a simple text message, or
90
+ no message at all.
91
+ @since 0.1.0
92
+ =end
93
+ class HTTPSimpleStatus < HTTPStatus
94
+
95
+ def initialize message = nil
96
+ /HTTP(\d\d\d)\w+\z/ === self.class.to_s
97
+ status = $1.to_i
98
+ super( status, message )
99
+ end
100
+
101
+ end
102
+
103
+
104
+ # @since 0.1.0
105
+ class HTTP201Created < HTTPStatus
106
+
107
+ def initialize locations
108
+ locations = [ locations ] unless locations.kind_of? Array
109
+ locations = locations.collect { |l| l.to_path }
110
+ rf = Request.current.resource_factory
111
+ if locations.size > 1
112
+ locations = locations.collect {
113
+ |l|
114
+ resource = rf[l]
115
+ { :location => l }.merge( resource.default_headers )
116
+ rf.uncache l if rf.respond_to? :uncache
117
+ }
118
+ super(
119
+ 201, 'New resources were created:', :locations => locations
120
+ )
121
+ else
122
+ location = locations[0]
123
+ resource = rf[location]
124
+ super(
125
+ 201, 'A new resource was created:', {
126
+ :location => location,
127
+ 'Location' => location
128
+ }.merge( resource.default_headers )
129
+ )
130
+ end
131
+ end
132
+
133
+ end # class HTTP201Created
134
+
135
+
136
+ # @since 0.1.0
137
+ class HTTP202Accepted < HTTPStatus
138
+
139
+ def initialize location = nil
140
+ if location
141
+ super(
142
+ 202, '', {
143
+ :'Job status location:' => Path.new(locations),
144
+ 'Location' => locations
145
+ }
146
+ )
147
+ else
148
+ super 202
149
+ end
150
+ end
151
+
152
+ end # class HTTP202Accepted
153
+
154
+
155
+ # @since 0.1.0
156
+ class HTTP301MovedPermanently < HTTPStatus
157
+
158
+ def initialize location
159
+ super(
160
+ 301, '', {
161
+ :'New location:' => Path.new(location),
162
+ 'Location' => location
163
+ }
164
+ )
165
+ end
166
+
167
+ end
168
+
169
+
170
+ # @since 0.1.0
171
+ class HTTP303SeeOther < HTTPStatus
172
+
173
+ def initialize location
174
+ super(
175
+ 303, '', {
176
+ :'See:' => Path.new(location),
177
+ 'Location' => location
178
+ }
179
+ )
180
+ end
181
+
182
+ end
183
+
184
+
185
+ # @since 0.1.0
186
+ class HTTP304NotModified < HTTPStatus
187
+
188
+ def initialize location
189
+ super( 304, nil, self.resource.default_headers )
190
+ end
191
+
192
+ end
193
+
194
+
195
+ # @since 0.1.0
196
+ class HTTP307TemporaryRedirect < HTTPStatus
197
+
198
+ def initialize location
199
+ super(
200
+ 301, '', {
201
+ :'Current location:' => Path.new(location),
202
+ 'Location' => location
203
+ }
204
+ )
205
+ end
206
+
207
+ end
208
+
209
+
210
+ # @since 0.1.0
211
+ class HTTP400BadRequest < HTTPSimpleStatus; end
212
+
213
+ # @since 0.1.0
214
+ class HTTP403Forbidden < HTTPSimpleStatus; end
215
+
216
+ # @since 0.1.0
217
+ class HTTP404NotFound < HTTPSimpleStatus; end
218
+
219
+
220
+ # @since 0.1.0
221
+ class HTTP405MethodNotAllowed < HTTPStatus
222
+
223
+ def initialize methods
224
+ super 405, '', 'Allow' => methods.join(', '),
225
+ :'Allowed methods:' => methods
226
+ end
227
+
228
+ end
229
+
230
+
231
+ # @since 0.1.0
232
+ class HTTP406NotAcceptable < HTTPStatus
233
+
234
+ def initialize content_types
235
+ super 406, '',
236
+ :'Available content-type(s):' => content_types
237
+ end
238
+
239
+ end
240
+
241
+
242
+ # @since 0.1.0
243
+ class HTTP409Conflict < HTTPSimpleStatus; end
244
+
245
+ # @since 0.1.0
246
+ class HTTP410Gone < HTTPSimpleStatus; end
247
+
248
+ # @since 0.1.0
249
+ class HTTP411LengthRequired < HTTPSimpleStatus; end
250
+
251
+
252
+ # @since 0.1.0
253
+ class HTTP412PreconditionFailed < HTTPStatus
254
+
255
+ def initialize header = nil
256
+ info =
257
+ if header
258
+ { header.to_sym => Request.current.env[ 'HTTP_' + header.gsub('-', '_').upcase ] }
259
+ else
260
+ {}
261
+ end
262
+ super 412, 'Failed precondition:', info
263
+ end
264
+
265
+ end
266
+
267
+
268
+ # @since 0.1.0
269
+ class HTTP415UnsupportedMediaType < HTTPStatus
270
+
271
+ def initialize media_types
272
+ super 405, '',
273
+ :'Supported media-type(s):' => media_types
274
+ end
275
+
276
+ end
277
+
278
+
279
+ # @since 0.1.0
280
+ class HTTP422UnprocessableEntity < HTTPSimpleStatus; end
281
+
282
+ # @since 0.1.0
283
+ class HTTP501NotImplemented < HTTPSimpleStatus; end
284
+
285
+ # @since 0.1.0
286
+ class HTTP503ServiceUnavailable < HTTPSimpleStatus; end
287
+
288
+ end # module Rackful
@@ -0,0 +1,112 @@
1
+ # Required for parsing:
2
+
3
+ # Required for running:
4
+ require 'rack/utils'
5
+
6
+
7
+ # A String monkeypatch
8
+ # @private
9
+ class String
10
+
11
+ def to_path; Rackful::Path.new(self); end
12
+
13
+ end
14
+
15
+
16
+ module Rackful
17
+
18
+
19
+ # Relative URI (a path)
20
+ class Path < String
21
+
22
+ def slashify
23
+ r = self.dup
24
+ r << '/' if '/' != r[-1,1]
25
+ r
26
+ end
27
+
28
+ def slashify!
29
+ if '/' != self[-1,1]
30
+ self << '/'
31
+ else
32
+ nil
33
+ end
34
+ end
35
+
36
+ def unslashify
37
+ r = self.dup
38
+ r = r.chomp( '/' ) if '/' == r[-1,1]
39
+ r
40
+ end
41
+
42
+ def unslashify!
43
+ if '/' == self[-1,1]
44
+ self.chomp! '/'
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ # An alias for Rack::Utils.unescape
51
+ def unescape( encoding = Encoding::UTF_8 ); Rack::Utils.unescape(self, encoding); end
52
+
53
+ end # class Path
54
+
55
+
56
+ end # module Rackful
57
+
58
+ =begin comment
59
+ # Monkeypatch to this stdlib class.
60
+ class URI::Generic
61
+
62
+ # @see http://www.w3.org/TR/html401/struct/links.html#adef-rel the HTML `rel` attribute.
63
+ attr_accessor :rel
64
+
65
+ # @see http://www.w3.org/TR/html401/struct/links.html#adef-rev the HTML `rev` attribute.
66
+ attr_accessor :rev
67
+
68
+ def to_xhtml base_path, encoding = Encoding::UTF_8
69
+ retval = "<a href=\"#{self.route_from(base_path)}\"".encode encoding
70
+ retval << " rel=\"#{self.rel}\"" if self.rel
71
+ retval << " rev=\"#{self.rev}\"" if self.rev
72
+ retval << '>'
73
+ if self.relative? && ! self.query && ! self.fragment
74
+ retval << Rack::Utils.escape_html(
75
+ Rack::Utils.unescape( self.route_from(base_path).to_s, encoding )
76
+ )
77
+ else
78
+ retval << self.to_s
79
+ end
80
+ retval << '</a>'
81
+ retval
82
+ end
83
+
84
+ # @return [URI::Generic]
85
+ def slashify
86
+ r = self.dup
87
+ r.path = r.path.unslashify
88
+ r
89
+ end
90
+
91
+ # @return [self, nil]
92
+ def slashify!
93
+ self.path.slashify! && self
94
+ end
95
+
96
+ # @return [URI::Generic]
97
+ def unslashify
98
+ r = self.dup
99
+ r.path = r.path.unslashify
100
+ r
101
+ end
102
+
103
+ # @return [self, nil]
104
+ def unslashify!
105
+ self.path.unslashify! && self
106
+ end
107
+
108
+ end # class ::URI::Generic
109
+ =end comment
110
+
111
+
112
+
@@ -0,0 +1,268 @@
1
+ # Required for parsing:
2
+ require 'rack'
3
+
4
+ # Required for running:
5
+
6
+
7
+ module Rackful
8
+
9
+ =begin markdown
10
+ Subclass of {Rack::Request}, augmented for Rackful requests.
11
+ @since 0.0.1
12
+ =end
13
+ class Request < Rack::Request
14
+
15
+
16
+ =begin markdown
17
+ The resource factory for the current request.
18
+ @return [#[]]
19
+ @see Server#initialize
20
+ @since 0.0.1
21
+ =end
22
+ def resource_factory; self.env['rackful.resource_factory']; end
23
+ def base_path
24
+ self.env['rackful.base_path'] ||= begin
25
+ r = self.content_path.dup
26
+ r[%r{[^/]*\z}] = ''
27
+ r
28
+ end
29
+ end
30
+ =begin markdown
31
+ Similar to the HTTP/1.1 `Content-Location:` header. Contains the canonical path
32
+ to the requested resource, which may differ from {#path}
33
+ @return [Path]
34
+ @since 0.1.0
35
+ =end
36
+ def content_path; self.env['rackful.content_path'] ||= self.path; end
37
+ =begin markdown
38
+ Set by {Rackful::Server#call!}
39
+ @return [Path]
40
+ @since 0.1.0
41
+ =end
42
+ def content_path= bp; self.env['rackful.content_path'] = bp.to_path; end
43
+ =begin markdown
44
+ @return [Path]
45
+ @since 0.1.0
46
+ =end
47
+ def path; super.to_path; end
48
+
49
+
50
+ def initialize resource_factory, *args
51
+ super( *args )
52
+ self.env['rackful.resource_factory'] = resource_factory
53
+ end
54
+
55
+
56
+ =begin markdown
57
+ The request currently being processed in the current thread.
58
+
59
+ In a multi-threaded server, multiple requests can be handled at one time.
60
+ This method returns the request object, created (and registered) by
61
+ {Server#call!}
62
+ @return [Request]
63
+ @since 0.0.1
64
+ =end
65
+ def self.current
66
+ Thread.current[:rackful_request]
67
+ end
68
+
69
+
70
+ =begin markdown
71
+ Assert all <tt>If-*</tt> request headers.
72
+ @return [void]
73
+ @raise [HTTP304NotModified, HTTP400BadRequest, HTTP404NotFound, HTTP412PreconditionFailed]
74
+ with the following meanings:
75
+
76
+ - `304 Not Modified`
77
+ - `400 Bad Request` Couldn't parse one or more <tt>If-*</tt> headers, or a
78
+ weak validator comparison was requested for methods other than `GET` or
79
+ `HEAD`.
80
+ - `404 Not Found`
81
+ - `412 Precondition Failed`
82
+ @see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616, section 13.3.3
83
+ for details about weak and strong validator comparison.
84
+ @todo Implement support for the `If-Range:` header.
85
+ @since 0.0.1
86
+ =end
87
+ def assert_if_headers resource
88
+ #raise HTTP501NotImplemented, 'If-Range: request header is not supported.' \
89
+ # if @env.key? 'HTTP_IF_RANGE'
90
+ empty = resource.empty?
91
+ etag =
92
+ if ! empty && resource.respond_to?(:get_etag)
93
+ resource.get_etag
94
+ else
95
+ nil
96
+ end
97
+ last_modified =
98
+ if ! empty && resource.respond_to?(:get_last_modified)
99
+ resource.get_last_modified
100
+ else
101
+ nil
102
+ end
103
+ cond = {
104
+ :match => self.if_match,
105
+ :none_match => self.if_none_match,
106
+ :modified_since => self.if_modified_since,
107
+ :unmodified_since => self.if_unmodified_since
108
+ }
109
+ allow_weak = ['GET', 'HEAD'].include? self.request_method
110
+ if empty
111
+ if cond[:match]
112
+ raise HTTP412PreconditionFailed, 'If-Match'
113
+ elsif cond[:unmodified_since]
114
+ raise HTTP412PreconditionFailed, 'If-Unmodified-Since'
115
+ elsif cond[:modified_since]
116
+ raise HTTP404NotFound
117
+ end
118
+ else
119
+ if cond[:none_match] && self.validate_etag( etag, cond[:none_match] )
120
+ raise HTTP412PreconditionFailed, 'If-None-Match'
121
+ elsif cond[:match] && ! self.validate_etag( etag, cond[:match] )
122
+ raise HTTP412PreconditionFailed, 'If-Match'
123
+ elsif cond[:unmodified_since]
124
+ if ! last_modified || cond[:unmodified_since] < last_modified[0]
125
+ raise HTTP412PreconditionFailed, 'If-Unmodified-Since'
126
+ elsif last_modified && ! last_modified[1] && ! allow_weak &&
127
+ cond[:unmodified_since] == last_modified[0]
128
+ raise HTTP412PreconditionFailed, 'If-Unmodified-Since'
129
+ end
130
+ elsif cond[:modified_since]
131
+ if ! last_modified || cond[:modified_since] >= last_modified[0]
132
+ raise HTTP304NotModified
133
+ elsif last_modified && ! last_modified[1] && !allow_weak &&
134
+ cond[:modified_since] == last_modified[0]
135
+ raise HTTP412PreconditionFailed, 'If-Modified-Since'
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+
142
+ =begin markdown
143
+ Hash of acceptable media types and their qualities.
144
+
145
+ This method parses the HTTP/1.1 `Accept:` header. If no acceptable media
146
+ types are provided, an empty Hash is returned.
147
+ @return [Hash{media_type => quality}]
148
+ @since 0.0.1
149
+ =end
150
+ def accept
151
+ @env['rackful.accept'] ||= begin
152
+ Hash[
153
+ @env['HTTP_ACCEPT'].to_s.split(',').collect do
154
+ |entry|
155
+ type, *options = entry.delete(' ').split(';')
156
+ quality = 1
157
+ options.each { |e|
158
+ quality = e[2..-1].to_f if e.start_with? 'q='
159
+ }
160
+ [type, quality]
161
+ end
162
+ ]
163
+ rescue
164
+ {}
165
+ end
166
+ end # def accept
167
+
168
+
169
+ =begin markdown
170
+ @!method if_match()
171
+ Parses the HTTP/1.1 `If-Match:` header.
172
+ @return [nil, Array<String>]
173
+ @see http://tools.ietf.org/html/rfc2616#section-14.24 RFC2616, section 14.24
174
+ @see #if_none_match
175
+ @since 0.0.1
176
+ =end
177
+ def if_match none = false
178
+ header = @env["HTTP_IF_#{ none ? 'NONE_' : '' }MATCH"]
179
+ return nil unless header
180
+ envkey = "rackful.if_#{ none ? 'none_' : '' }match"
181
+ if %r{\A\s*\*\s*\z} === header
182
+ return [ '*' ]
183
+ elsif %r{\A(\s*(W/)?"([^"\\]|\\.)*"\s*,)+\z}m === ( header + ',' )
184
+ return header.scan( %r{(?:W/)?"(?:[^"\\]|\\.)*"}m )
185
+ end
186
+ raise HTTP400BadRequest, "Couldn't parse If-#{ none ? 'None-' : '' }Match: #{header}"
187
+ end
188
+
189
+
190
+ =begin markdown
191
+ Parses the HTTP/1.1 `If-None-Match:` header.
192
+ @return [nil, Array<String>]
193
+ @see http://tools.ietf.org/html/rfc2616#section-14.26 RFC2616, section 14.26
194
+ @see #if_match
195
+ @since 0.0.1
196
+ =end
197
+ def if_none_match
198
+ self.if_match true
199
+ end
200
+
201
+
202
+ =begin markdown
203
+ @!method if_modified_since()
204
+ @return [nil, Time]
205
+ @see http://tools.ietf.org/html/rfc2616#section-14.25 RFC2616, section 14.25
206
+ @see #if_unmodified_since
207
+ @since 0.0.1
208
+ =end
209
+ def if_modified_since unmodified = false
210
+ header = @env["HTTP_IF_#{ unmodified ? 'UN' : '' }MODIFIED_SINCE"]
211
+ return nil unless header
212
+ begin
213
+ header = Time.httpdate( header )
214
+ rescue ArgumentError
215
+ raise HTTP400BadRequest, "Couldn't parse If-#{ unmodified ? 'Unmodified' : 'Modified' }-Since: #{header}"
216
+ end
217
+ header
218
+ end
219
+
220
+
221
+ =begin markdown
222
+ @return [nil, Time]
223
+ @see http://tools.ietf.org/html/rfc2616#section-14.28 RFC2616, section 14.28
224
+ @see #if_modified_since
225
+ @since 0.0.1
226
+ =end
227
+ def if_unmodified_since
228
+ self.if_modified_since true
229
+ end
230
+
231
+
232
+ =begin markdown
233
+ Does any of the tags in `etags` match `etag`?
234
+ @param etag [#to_s]
235
+ @param etags [#to_a]
236
+ @example
237
+ etag = '"foo"'
238
+ etags = [ 'W/"foo"', '"bar"' ]
239
+ validate_etag etag, etags
240
+ #> true
241
+ @return [Boolean]
242
+ @see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616 section 13.3.3
243
+ for details about weak and strong validator comparison.
244
+ @since 0.0.1
245
+ =end
246
+ def validate_etag etag, etags
247
+ etag = etag.to_s
248
+ match = etags.to_a.detect do
249
+ |tag|
250
+ tag = tag.to_s
251
+ tag == '*' or
252
+ tag == etag or
253
+ 'W/' + tag == etag or
254
+ 'W/' + etag == tag
255
+ end
256
+ if match and
257
+ '*' != match and
258
+ 'W/' == etag[0,2] || 'W/' == match[0,2] and
259
+ ! [ 'HEAD', 'GET' ].include? self.request_method
260
+ raise HTTP400BadRequest, "Weak validators are only allowed for GET and HEAD requests."
261
+ end
262
+ !!match
263
+ end
264
+
265
+
266
+ end # class Request
267
+
268
+ end # module Rackful