rackful 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGES.md +18 -8
- data/RACKFUL.md +46 -34
- data/README.md +11 -5
- data/example/config.ru +19 -25
- data/lib/rackful.rb +11 -2
- data/lib/rackful/httpstatus.rb +250 -0
- data/lib/rackful/middleware.rb +2 -3
- data/lib/rackful/middleware/headerspoofing.rb +49 -0
- data/lib/rackful/middleware/methodoverride.rb +136 -0
- data/lib/rackful/parser.rb +315 -0
- data/lib/rackful/request.rb +103 -53
- data/lib/rackful/resource.rb +158 -221
- data/lib/rackful/serializer.rb +133 -215
- data/lib/rackful/server.rb +76 -86
- data/lib/rackful/uri.rb +150 -0
- data/mkdoc.sh +4 -2
- data/rackful.gemspec +6 -5
- metadata +66 -58
- data/lib/rackful/http_status.rb +0 -285
- data/lib/rackful/middleware/header_spoofing.rb +0 -72
- data/lib/rackful/middleware/method_spoofing.rb +0 -101
- data/lib/rackful/middleware/relative_location.rb +0 -71
- data/lib/rackful/path.rb +0 -179
data/lib/rackful/serializer.rb
CHANGED
@@ -1,240 +1,204 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Required for running:
|
4
|
-
require 'rack/utils'
|
5
|
-
require 'uri'
|
6
|
-
require 'base64'
|
7
|
-
require 'json'
|
8
|
-
require 'time'
|
9
|
-
#require 'json/pure'
|
1
|
+
# encoding: utf-8
|
10
2
|
|
11
3
|
|
12
4
|
module Rackful
|
13
5
|
|
14
6
|
|
15
|
-
=begin markdown
|
16
|
-
Base class for all serializers.
|
17
7
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
8
|
+
# Base class for all serializers.
|
9
|
+
#
|
10
|
+
# The serializers {Serializer::XHTML} and {Serializer::JSON} defined in this
|
11
|
+
# library depend on the presence of method
|
12
|
+
# {Rackful::Resource#to_rackful resource.to_rackful}.
|
13
|
+
# @abstract Subclasses must implement method `#each` end define constant
|
14
|
+
# `CONTENT_TYPES`
|
15
|
+
# @!attribute [r] request
|
16
|
+
# @return [Request]
|
17
|
+
# @!attribute [r] resource
|
18
|
+
# @return [Resource]
|
19
|
+
# @!attribute [r] content_type
|
20
|
+
# @return [String] The content type to be served by this Serializer. This will
|
21
|
+
# always be one of the content types listed in constant `CONTENT_TYPES`.
|
23
22
|
class Serializer
|
24
23
|
|
25
24
|
|
26
25
|
include Enumerable
|
27
26
|
|
28
27
|
|
29
|
-
attr_reader :resource, :content_type
|
30
|
-
|
28
|
+
attr_reader :request, :resource, :content_type
|
31
29
|
|
32
|
-
def initialize resource, content_type
|
33
|
-
@resource, @content_type = resource, content_type
|
34
|
-
end
|
35
30
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
@
|
41
|
-
|
42
|
-
def each
|
43
|
-
raise "Class #{self.class} doesn't implement #each()."
|
31
|
+
# @param request [Request]
|
32
|
+
# @param resource [Resource]
|
33
|
+
# @param content_type [String]
|
34
|
+
def initialize request, resource, content_type
|
35
|
+
@request, @resource, @content_type =
|
36
|
+
request, resource, content_type
|
44
37
|
end
|
45
38
|
|
46
39
|
|
47
|
-
|
48
|
-
|
49
|
-
|
40
|
+
# @!method headers()
|
41
|
+
# Extra response headers that a serializer likes to return.
|
42
|
+
#
|
43
|
+
# You don't have to include the `Content-Type` header, as this is done
|
44
|
+
# _for_ you.
|
45
|
+
#
|
46
|
+
# This method is optional.
|
47
|
+
# @return [Hash]
|
48
|
+
# @abstract
|
50
49
|
|
51
|
-
You don't have to include the `Content-Type` header, as this is done _for_ you.
|
52
50
|
|
53
|
-
|
54
|
-
@
|
55
|
-
|
56
|
-
|
51
|
+
# @abstract Every serializer must implement this method.
|
52
|
+
# @yieldparam block [String] (part of) the entity body
|
53
|
+
def each
|
54
|
+
raise NotImplementedError
|
55
|
+
end
|
57
56
|
|
58
57
|
|
59
58
|
end # class Serializer
|
60
59
|
|
61
60
|
|
62
|
-
|
63
|
-
=end
|
64
|
-
class XHTML < Serializer
|
61
|
+
class Serializer::XHTML < Serializer
|
65
62
|
|
66
63
|
|
67
64
|
# The content types served by this serializer.
|
68
65
|
# @see Serializer::CONTENT_TYPES
|
69
66
|
CONTENT_TYPES = [
|
70
|
-
'application/
|
71
|
-
'text/html; charset=UTF-8',
|
67
|
+
'application/xml; charset=UTF-8',
|
72
68
|
'text/xml; charset=UTF-8',
|
73
|
-
'
|
69
|
+
'text/html; charset=UTF-8',
|
70
|
+
'application/xhtml+xml; charset=UTF-8',
|
74
71
|
]
|
72
|
+
|
73
|
+
# @api private
|
74
|
+
# @return [URI::HTTP]
|
75
|
+
def html_base_uri
|
76
|
+
@html_base_uri ||= begin
|
77
|
+
retval = self.request.canonical_uri.dup
|
78
|
+
retval.path = retval.path.sub( %r{[^/]+\z}, '' )
|
79
|
+
retval.query = nil
|
80
|
+
retval
|
81
|
+
end
|
82
|
+
end
|
75
83
|
|
76
|
-
|
77
|
-
=begin
|
78
|
-
@yieldparam xhtml [String]
|
79
|
-
=end
|
84
|
+
# @yieldparam xhtml [String]
|
80
85
|
def each &block
|
81
|
-
|
86
|
+
tmp = ''
|
87
|
+
# The XML header is only sent for XML media types:
|
82
88
|
if /xml/ === self.content_type
|
83
|
-
|
89
|
+
tmp += <<EOS
|
84
90
|
<?xml version="1.0" encoding="UTF-8"?>
|
85
91
|
EOS
|
86
92
|
end
|
87
|
-
|
93
|
+
tmp += <<EOS
|
88
94
|
<!DOCTYPE html>
|
89
95
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
90
96
|
<head>
|
91
|
-
<title>#{ Rack::Utils.escape_html(resource.title) }</title>
|
97
|
+
<title>#{ Rack::Utils.escape_html(self.resource.title) }</title>
|
98
|
+
<base href="#{self.html_base_uri}"/>
|
92
99
|
EOS
|
93
|
-
|
94
|
-
|
95
|
-
<
|
96
|
-
EOS
|
97
|
-
#~ end
|
98
|
-
unless '/' == request.path
|
99
|
-
yield <<EOS
|
100
|
-
<link rel="contents" href="#{'/' === request.content_path[-1] ? '../' : './' }"/>
|
100
|
+
unless '/' == self.request.canonical_uri.path
|
101
|
+
tmp += <<EOS
|
102
|
+
<link rel="contents" href="#{'/' === self.request.canonical_uri.path[-1] ? '../' : './' }"/>
|
101
103
|
EOS
|
102
104
|
end
|
103
|
-
|
104
|
-
|
105
|
+
r = self.resource.to_rackful
|
106
|
+
tmp += self.header + '<div id="rackful-content"' + self.xsd_type( r ) + '>'
|
107
|
+
yield tmp
|
108
|
+
each_nested( r, &block )
|
105
109
|
yield '</div>' + footer
|
106
110
|
end
|
107
111
|
|
108
112
|
|
109
|
-
#
|
113
|
+
# @api private
|
110
114
|
def header
|
111
115
|
self.class.class_variable_defined?( :@@header ) && @@header ?
|
112
116
|
@@header.call( self ) :
|
113
117
|
"</head><body>"
|
114
118
|
end
|
115
119
|
|
116
|
-
|
120
|
+
|
121
|
+
# Set a header generator.
|
122
|
+
# @yieldparam serializer [Serializer::XHTML] This serializer
|
123
|
+
# @yieldreturn [String] some XHTML
|
117
124
|
def self.header &block
|
118
125
|
@@header = block
|
119
126
|
self
|
120
127
|
end
|
121
128
|
|
122
129
|
|
123
|
-
#
|
130
|
+
# @api private
|
124
131
|
def footer
|
125
132
|
self.class.class_variable_defined?( :@@footer ) && @@footer ?
|
126
133
|
@@footer.call( self ) :
|
127
|
-
'<div class="
|
134
|
+
'<div class="rackful-powered">Powered by <a href="http://github.com/pieterb/Rackful">Rackful</a></div></body></html>'
|
128
135
|
end
|
129
136
|
|
130
|
-
|
137
|
+
|
138
|
+
# Set a footer generator.
|
139
|
+
# @yieldparam serializer [Serializer::XHTML] This serializer
|
140
|
+
# @yieldreturn [String] some XHTML
|
131
141
|
def self.footer &block
|
132
142
|
@@footer = block
|
133
143
|
self
|
134
144
|
end
|
135
145
|
|
136
146
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
*
|
143
|
-
|
144
|
-
|
145
|
-
*
|
146
|
-
|
147
|
-
|
148
|
-
*
|
149
|
-
|
150
|
-
*
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
@
|
156
|
-
@yieldparam xhtml[String]
|
157
|
-
=end
|
147
|
+
# Serializes many kinds of objects to XHTML.
|
148
|
+
#
|
149
|
+
# How an object is serialized, depends:
|
150
|
+
#
|
151
|
+
# * A *{Resource}* will be serialized by its own {Resource#serializer serializer}.
|
152
|
+
# * A *{URI}* will be serialized as a hyperlink.
|
153
|
+
# * An Object responding to *`#each_pair`* (i.e. something {Hash}-like) will
|
154
|
+
# be represented by
|
155
|
+
# * a descriptive list, with
|
156
|
+
# * An Object responding to *`#each`* (i.e. something {Enumerable}) will
|
157
|
+
# be represented as a JSON array.
|
158
|
+
# * A *binary encoded {String}* (i.e. a blob} is represented by a JSON string,
|
159
|
+
# containing the base64 encoded version of the data.
|
160
|
+
# * A *{Time}* is represented by a string containing a dateTime as defined by
|
161
|
+
# XMLSchema.
|
162
|
+
# * On *all the rest,* method `#to_json` is invoked.
|
163
|
+
# @overload each_nested
|
164
|
+
# @yieldparam xhtml [String]
|
165
|
+
# @api private
|
158
166
|
def each_nested p = self.resource.to_rackful, &block
|
159
|
-
|
167
|
+
|
168
|
+
# A Resource:
|
160
169
|
if p.kind_of?( Resource ) && ! p.equal?( self.resource )
|
161
|
-
p.serializer( self.content_type ).each_nested &block
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
) ) + '</a>'
|
168
|
-
|
169
|
-
#
|
170
|
-
# if p.recurse?
|
171
|
-
# yield '<dl class="rackful-resources">'
|
172
|
-
# p.each_pair do
|
173
|
-
# |path, child|
|
174
|
-
# yield '<dt>'
|
175
|
-
# self.each_nested path, &block
|
176
|
-
# yield "</dt><dd#{self.xsd_type(child)}>"
|
177
|
-
# self.each_nested child, &block
|
178
|
-
# yield "</dd>\n"
|
179
|
-
# end
|
180
|
-
# yield '</dl>'
|
181
|
-
#~ elsif ( q = p.first ) and
|
182
|
-
#~ q.kind_of?( Enumerable )
|
183
|
-
#~ q.respond_to?( :keys ) && ( keys = q.keys ) &&
|
184
|
-
#~ p.all? { |r| r.respond_to?( :keys ) && r.keys == keys }
|
185
|
-
#~ )
|
186
|
-
#~ yield '<table class="rackful-objects"><thead><tr>' +
|
187
|
-
#~ keys.collect {
|
188
|
-
#~ |column|
|
189
|
-
#~ '<th>' +
|
190
|
-
#~ Rack::Utils.escape_html( column.to_s.split('_').join(' ') ) +
|
191
|
-
#~ "</th>\n"
|
192
|
-
#~ }.join + '</tr></thead><tbody>'
|
193
|
-
#~ p.each do
|
194
|
-
#~ |h|
|
195
|
-
#~ yield '<tr>'
|
196
|
-
#~ h.each_pair do
|
197
|
-
#~ |key, value|
|
198
|
-
#~ yield "<td class=\"rackful-objects-#{Rack::Utils.escape_html( key.to_s )}\"#{self.xsd_type(value)}>"
|
199
|
-
#~ self.each_nested value, &block
|
200
|
-
#~ yield "</td>\n"
|
201
|
-
#~ end
|
202
|
-
#~ yield '</tr>'
|
203
|
-
#~ end
|
204
|
-
#~ yield "</tbody></table>"
|
205
|
-
# else
|
206
|
-
# yield '<ul class="rackful-resources">'
|
207
|
-
# p.each do
|
208
|
-
# |value|
|
209
|
-
# yield "<li#{self.xsd_type(value)}>"
|
210
|
-
# self.each_nested value, &block
|
211
|
-
# yield "</li>\n"
|
212
|
-
# end
|
213
|
-
# yield '</ul>'
|
214
|
-
# end
|
215
|
-
|
170
|
+
p.serializer( self.request, self.content_type ).each_nested( &block )
|
171
|
+
|
172
|
+
# A URI:
|
173
|
+
elsif p.kind_of?( URI )
|
174
|
+
rel_path = p.relative? ? p : p.route_from( self.html_base_uri )
|
175
|
+
yield "<a href=\"#{rel_path}\">" +
|
176
|
+
Rack::Utils.escape_html( Rack::Utils.unescape( rel_path.to_s ) ) + '</a>'
|
177
|
+
|
178
|
+
# An Object:
|
216
179
|
elsif p.respond_to?( :each_pair )
|
217
|
-
yield '<br/><dl
|
180
|
+
yield '<br/><dl>'
|
218
181
|
p.each_pair do
|
219
182
|
|key, value|
|
220
|
-
yield
|
221
|
-
|
222
|
-
|
183
|
+
yield '<dt xs:type="xs:string">' +
|
184
|
+
Rack::Utils.escape_html( key.to_s.split('_').join(' ') ) +
|
185
|
+
"</dt><dd#{self.xsd_type(value)}>"
|
223
186
|
self.each_nested value, &block
|
224
187
|
yield "</dd>\n"
|
225
188
|
end
|
226
189
|
yield '</dl>'
|
227
|
-
|
190
|
+
|
191
|
+
# A List of Objects with identical keys:
|
228
192
|
elsif p.kind_of?( Enumerable ) and
|
229
193
|
( q = p.first ) and
|
230
194
|
(
|
231
195
|
q.respond_to?( :keys ) && ( keys = q.keys ) &&
|
232
196
|
p.all? { |r| r.respond_to?( :keys ) && r.keys == keys }
|
233
197
|
)
|
234
|
-
yield '<table
|
198
|
+
yield '<table><thead><tr>' +
|
235
199
|
keys.collect {
|
236
200
|
|column|
|
237
|
-
'<th>' +
|
201
|
+
'<th xs:type="xs:string">' +
|
238
202
|
Rack::Utils.escape_html( column.to_s.split('_').join(' ') ) +
|
239
203
|
"</th>\n"
|
240
204
|
}.join + '</tr></thead><tbody>'
|
@@ -243,16 +207,17 @@ How an object is serialized, depends:
|
|
243
207
|
yield '<tr>'
|
244
208
|
h.each_pair do
|
245
209
|
|key, value|
|
246
|
-
yield "<td
|
210
|
+
yield "<td#{self.xsd_type(value)}>"
|
247
211
|
self.each_nested value, &block
|
248
212
|
yield "</td>\n"
|
249
213
|
end
|
250
214
|
yield '</tr>'
|
251
215
|
end
|
252
216
|
yield "</tbody></table>"
|
253
|
-
|
217
|
+
|
218
|
+
# A List:
|
254
219
|
elsif p.kind_of?( Enumerable )
|
255
|
-
yield '<ul
|
220
|
+
yield '<ul>'
|
256
221
|
p.each do
|
257
222
|
|value|
|
258
223
|
yield "<li#{self.xsd_type(value)}>"
|
@@ -260,21 +225,24 @@ How an object is serialized, depends:
|
|
260
225
|
yield "</li>\n"
|
261
226
|
end
|
262
227
|
yield '</ul>'
|
263
|
-
|
228
|
+
|
229
|
+
# A Time:
|
264
230
|
elsif p.kind_of?( Time )
|
265
231
|
yield p.utc.xmlschema
|
266
|
-
|
232
|
+
|
233
|
+
# A Blob:
|
267
234
|
elsif p.kind_of?( String ) && p.encoding == Encoding::BINARY
|
268
235
|
yield Base64.encode64(p).chomp
|
269
|
-
|
236
|
+
|
237
|
+
# Something serializable (including nil, true, false, Numeric):
|
270
238
|
else
|
271
239
|
yield Rack::Utils.escape_html( p.to_s )
|
272
|
-
|
240
|
+
|
273
241
|
end
|
274
242
|
end
|
275
243
|
|
276
244
|
|
277
|
-
# @private
|
245
|
+
# @api private
|
278
246
|
def xsd_type v
|
279
247
|
if v.respond_to? :to_rackful
|
280
248
|
v = v.to_rackful
|
@@ -289,7 +257,7 @@ How an object is serialized, depends:
|
|
289
257
|
' xs:type="xs:dateTime"'
|
290
258
|
elsif v.kind_of?( String ) && v.encoding == Encoding::BINARY
|
291
259
|
' xs:type="xs:base64Binary"'
|
292
|
-
elsif v.kind_of?( String )
|
260
|
+
elsif v.kind_of?( String )
|
293
261
|
' xs:type="xs:string"'
|
294
262
|
else
|
295
263
|
''
|
@@ -297,10 +265,10 @@ How an object is serialized, depends:
|
|
297
265
|
end
|
298
266
|
|
299
267
|
|
300
|
-
end # class XHTML
|
268
|
+
end # class Serializer::XHTML
|
301
269
|
|
302
270
|
|
303
|
-
class JSON < Serializer
|
271
|
+
class Serializer::JSON < Serializer
|
304
272
|
|
305
273
|
|
306
274
|
CONTENT_TYPES = [
|
@@ -309,31 +277,11 @@ class JSON < Serializer
|
|
309
277
|
]
|
310
278
|
|
311
279
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
How an object is serialized, depends:
|
316
|
-
|
317
|
-
* A *{Resource}* will be serialized by its own
|
318
|
-
{Resource#serializer serializer}.
|
319
|
-
* A *{Path}* will be serialized by a string, containing the relative path.
|
320
|
-
* An Object responding to *`#each_pair`* (i.e. something {Hash}-like) will
|
321
|
-
be represented as a JSON object.
|
322
|
-
* An Object responding to *`#each`* (i.e. something {Enumerable}) will
|
323
|
-
be represented as a JSON array.
|
324
|
-
* A *binary encoded {String}* (i.e. a blob} is represented by a JSON string,
|
325
|
-
containing the base64 encoded version of the data.
|
326
|
-
* A *{Time}* is represented by a string containing a dateTime as defined by
|
327
|
-
XMLSchema.
|
328
|
-
* On *all the rest,* method `#to_json` is invoked.
|
329
|
-
@overload each
|
330
|
-
@yieldparam json [String]
|
331
|
-
=end
|
280
|
+
# @yield [json]
|
281
|
+
# @yieldparam json [String]
|
332
282
|
def each thing = self.resource.to_rackful, &block
|
333
283
|
if thing.kind_of?( Resource ) && ! thing.equal?( self.resource )
|
334
|
-
thing.serializer( self.content_type ).each &block
|
335
|
-
elsif thing.kind_of?( Path )
|
336
|
-
yield thing.relative.to_json
|
284
|
+
thing.serializer( self.content_type ).each( &block )
|
337
285
|
elsif thing.respond_to? :each_pair
|
338
286
|
first = true
|
339
287
|
thing.each_pair do
|
@@ -362,37 +310,7 @@ How an object is serialized, depends:
|
|
362
310
|
end
|
363
311
|
|
364
312
|
|
365
|
-
|
366
|
-
r = ::JSON.parse(
|
367
|
-
input.read,
|
368
|
-
:symbolize_names => true
|
369
|
-
)
|
370
|
-
self.recursive_datetime_parser r
|
371
|
-
end
|
372
|
-
|
373
|
-
|
374
|
-
def self.recursive_datetime_parser p
|
375
|
-
if p.kind_of?(String)
|
376
|
-
begin
|
377
|
-
return Time.xmlschema(p)
|
378
|
-
rescue
|
379
|
-
end
|
380
|
-
elsif p.kind_of?(Hash)
|
381
|
-
p.keys.each do
|
382
|
-
|key|
|
383
|
-
p[key] = self.recursive_datetime_parser( p[key] )
|
384
|
-
end
|
385
|
-
elsif p.kind_of?(Array)
|
386
|
-
(0 ... p.size).each do
|
387
|
-
|i|
|
388
|
-
p[i] = self.recursive_datetime_parser( p[i] )
|
389
|
-
end
|
390
|
-
end
|
391
|
-
p
|
392
|
-
end
|
393
|
-
|
394
|
-
|
395
|
-
end # class HTTPStatus::JSON
|
313
|
+
end # class Serializer::JSON
|
396
314
|
|
397
315
|
|
398
316
|
end # module Rackful
|