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,318 @@
1
+ # Required for parsing:
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'
10
+
11
+
12
+ module Rackful
13
+
14
+
15
+ =begin markdown
16
+ Base class for all serializers.
17
+
18
+ The default serializers defined in this library ({Rackful::XHTML} and {Rackful::JSON})
19
+ depend on the availability of method {Rackful::Resource#to_rackful.}
20
+ @abstract Subclasses must implement method `#each` end define constant
21
+ `CONTENT_TYPES`
22
+ @since 0.1.0
23
+ =end
24
+ class Serializer
25
+
26
+ include Enumerable
27
+
28
+ attr_reader :resource, :content_type
29
+
30
+ # @since 0.1.0
31
+ def initialize resource, content_type
32
+ @resource, @content_type = resource, content_type
33
+ end
34
+
35
+ =begin markdown
36
+ Every serializer must implement this method.
37
+ @abstract
38
+ @since 0.1.0
39
+ =end
40
+ def each
41
+ raise HTTP500InternalServerError, "Class #{self.class} doesn't implement #each()."
42
+ end
43
+
44
+ =begin markdown
45
+ You don't have to include the `Content-Type` header, as this is done _for_ you.
46
+
47
+ This method is optional.
48
+ @!method headers()
49
+ @return [Hash, nil]
50
+ @abstract
51
+ @since 0.1.0
52
+ =end
53
+
54
+
55
+ =begin markdown
56
+ The content types this serializer can produce.
57
+ @!const CONTENT_TYPES
58
+ @return [(String)]
59
+ @abstract
60
+ @since 0.1.0
61
+ =end
62
+
63
+ end # class Serializer
64
+
65
+
66
+ =begin markdown
67
+ @since 0.1.0
68
+ =end
69
+ class XHTML < Serializer
70
+
71
+ # The content types served by this serializer.
72
+ # @see Serializer::CONTENT_TYPES
73
+ CONTENT_TYPES = [
74
+ 'application/xhtml+xml; charset=UTF-8',
75
+ 'text/html; charset=UTF-8',
76
+ 'text/xml; charset=UTF-8',
77
+ 'application/xml; charset=UTF-8'
78
+ ]
79
+
80
+
81
+ # Turns a relative URI (starting with `/`) into a relative path (starting with `./`)
82
+ # @param path [Path]
83
+ # @return [String]
84
+ # @since 0.1.0
85
+ def htmlify path
86
+ @rackful_bp ||= Request.current.base_path # caching
87
+ length = @rackful_bp.length
88
+ if @rackful_bp == path[0, length]
89
+ './' + path[length .. -1]
90
+ else
91
+ path.dup
92
+ end
93
+ end
94
+
95
+
96
+ def each &block
97
+ request = Request.current
98
+ if /xml/ === self.content_type
99
+ yield <<EOS
100
+ <?xml version="1.0" encoding="UTF-8"?>
101
+ EOS
102
+ end
103
+ yield <<EOS
104
+ <!DOCTYPE html>
105
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:xs="http://www.w3.org/2001/XMLSchema">
106
+ <head>
107
+ EOS
108
+ unless request.path == request.content_path
109
+ yield <<EOS
110
+ <base href="#{request.base_path}"/>
111
+ EOS
112
+ end
113
+ unless '/' == request.path
114
+ yield <<EOS
115
+ <link rel="contents" href="#{File::dirname(request.path).to_path.slashify}"/>
116
+ EOS
117
+ end
118
+ yield header + '<div id="rackful_content">'
119
+ each_nested &block
120
+ yield '</div>' + footer
121
+ end
122
+
123
+ # Look at the source code!
124
+ def header
125
+ "<title>#{ Rack::Utils.escape_html(resource.title) }</title></head><body>"
126
+ end
127
+
128
+ # Look at the source code!
129
+ def footer
130
+ '<div class="rackful_powered">Powered by <a href="http://github.com/pieterb/Rackful">Rackful</a></div></body></html>'
131
+ end
132
+
133
+ # Serialize almost any kind of Ruby object to XHTML.
134
+ def each_nested p = self.resource.to_rackful, &block
135
+ # p = (args.size > 0) ? args[0] : self.resource.to_rackful
136
+ if p.kind_of?( Path )
137
+ yield "<a href=\"#{self.htmlify(p)}\">" +
138
+ Rack::Utils.escape_html( File::basename(p.unslashify).to_path.unescape ) +
139
+ '</a>'
140
+ elsif p.kind_of?( Resource ) && ! p.equal?( self.resource )
141
+ p.serializer( self.content_type ).each_nested &block
142
+ # elsif p.kind_of?( Hash )
143
+ # yield '<dl class="rackful_object">'
144
+ # p.each_pair do
145
+ # |key, value|
146
+ # yield '<dt>' + key.to_s.split('_').join(' ').escape_html +
147
+ # "</dt><dd class=\"rackful_object_#{key.to_s.escape_html}\"#{self.xsd_type(value)}>"
148
+ # self.each_nested value, &block
149
+ # yield "</dd>\n"
150
+ # end
151
+ # yield '</dl>'
152
+ elsif p.kind_of?( Enumerable ) and p.respond_to?( :each_pair ) and
153
+ p.all? { |r, s| r.kind_of?( Path ) }
154
+ yield '<dl class="rackful-resources">'
155
+ p.each_pair do
156
+ |path, child|
157
+ yield '<dt>'
158
+ self.each_nested path, &block
159
+ yield '</dt><dd>'
160
+ self.each_nested child, &block
161
+ yield "</dd>\n"
162
+ end
163
+ yield '</dl>'
164
+ elsif p.respond_to?( :each_pair )
165
+ yield '<dl class="rackful-object">'
166
+ p.each_pair do
167
+ |path, child|
168
+ yield '<dt>'
169
+ self.each_nested path, &block
170
+ yield '</dt><dd>'
171
+ self.each_nested child, &block
172
+ yield "</dd>\n"
173
+ end
174
+ yield '</dl>'
175
+ elsif p.kind_of?( Enumerable ) and ( q = p.first ) and (
176
+ q.respond_to?(:keys) && ( keys = q.keys ) &&
177
+ p.all? { |r| r.respond_to?(:keys) && r.keys == keys }
178
+ )
179
+ yield '<table class="rackful-objects"><thead><tr>' +
180
+ keys.collect {
181
+ |column|
182
+ '<th>' +
183
+ Rack::Utils.escape_html( column.to_s.split('_').join(' ') ) +
184
+ "</th>\n"
185
+ }.join + '</tr></thead><tbody>'
186
+ p.each do
187
+ |h|
188
+ yield '<tr>'
189
+ h.each_pair do
190
+ |key, value|
191
+ yield "<td class=\"rackful-objects-#{Rack::Utils.escape_html( key.to_s )}\"#{self.xsd_type(value)}>"
192
+ self.each_nested value, &block
193
+ yield "</td>\n"
194
+ end
195
+ yield '</tr>'
196
+ end
197
+ yield "</tbody></table>"
198
+ elsif p.kind_of?( Enumerable )
199
+ yield '<ul class="rackful-array">'
200
+ p.each do
201
+ |value|
202
+ yield "<li#{self.xsd_type(value)}>"
203
+ self.each_nested value, &block
204
+ yield "</li>\n"
205
+ end
206
+ yield '</ul>'
207
+ elsif p.kind_of?( Time )
208
+ yield p.utc.xmlschema
209
+ elsif p.kind_of?( String ) && p.encoding == Encoding::BINARY
210
+ yield Base64.encode64(p).chomp
211
+ else
212
+ yield Rack::Utils.escape_html( p.to_s )
213
+ end
214
+ end
215
+
216
+
217
+ # @private
218
+ def xsd_type v
219
+ if v.respond_to? :to_rackful
220
+ v = v.to_rackful
221
+ end
222
+ if [nil, true, false].include? v
223
+ ' xs:type="xs:boolean" xs:nil="true"'
224
+ elsif v.kind_of? Integer
225
+ ' xs:type="xs:integer"'
226
+ elsif v.kind_of? Numeric
227
+ ' xs:type="xs:decimal"'
228
+ elsif v.kind_of? Time
229
+ ' xs:type="xs:dateTime"'
230
+ elsif v.kind_of?( String ) && v.encoding == Encoding::BINARY
231
+ ' xs:type="xs:base64Binary"'
232
+ elsif v.kind_of?( String ) && !v.kind_of?( Path )
233
+ ' xs:type="xs:string"'
234
+ else
235
+ ''
236
+ end
237
+ end
238
+
239
+
240
+ end # class XHTML
241
+
242
+
243
+ class JSON < Serializer
244
+
245
+
246
+ CONTENT_TYPES = [
247
+ 'application/json',
248
+ 'application/x-json'
249
+ ]
250
+
251
+
252
+ =begin markdown
253
+ @yield [json]
254
+ @yieldparam json [String]
255
+ =end
256
+ def each thing = self.resource.to_rackful, &block
257
+ if thing.kind_of?( Resource ) && ! thing.equal?( self.resource )
258
+ thing.serializer( self.content_type ).each &block
259
+ elsif thing.respond_to? :each_pair
260
+ first = true
261
+ thing.each_pair do
262
+ |k, v|
263
+ yield( ( first ? "{\n" : ",\n" ) + k.to_s.to_json + ":" )
264
+ first = false
265
+ self.each v, &block
266
+ end
267
+ yield( first ? "{}" : "\n}" )
268
+ elsif thing.respond_to? :each
269
+ first = true
270
+ thing.each do
271
+ |v|
272
+ yield( first ? "[\n" : ",\n" )
273
+ first = false
274
+ self.each v, &block
275
+ end
276
+ yield( first ? "[]" : "\n]" )
277
+ elsif thing.kind_of?( String ) && thing.encoding == Encoding::BINARY
278
+ yield Base64.encode64(thing).chomp.to_json
279
+ elsif thing.kind_of?( Time )
280
+ yield thing.utc.xmlschema.to_json
281
+ else
282
+ yield thing.to_json
283
+ end
284
+ end
285
+
286
+
287
+ def self.parse input
288
+ r = ::JSON.parse(
289
+ input.read,
290
+ :symbolize_names => true
291
+ )
292
+ self.recursive_datetime_parser r
293
+ end
294
+
295
+ def self.recursive_datetime_parser p
296
+ if p.kind_of?(String)
297
+ begin
298
+ return Time.xmlschema(p)
299
+ rescue
300
+ end
301
+ elsif p.kind_of?(Hash)
302
+ p.keys.each do
303
+ |key|
304
+ p[key] = self.recursive_datetime_parser( p[key] )
305
+ end
306
+ elsif p.kind_of?(Array)
307
+ (0 ... p.size).each do
308
+ |i|
309
+ p[i] = self.recursive_datetime_parser( p[i] )
310
+ end
311
+ end
312
+ p
313
+ end
314
+
315
+
316
+ end # class HTTPStatus::JSON
317
+
318
+ end # module Rackful
@@ -0,0 +1,124 @@
1
+ # Required for parsing:
2
+ #require 'forwardable' # Used to be for ResourceFactoryWrapper.
3
+
4
+ # Required for running:
5
+
6
+ module Rackful
7
+
8
+ =begin markdown
9
+ Rack compliant server class for implementing RESTful web services.
10
+ @since 0.0.1
11
+ =end
12
+ class Server
13
+
14
+
15
+ =begin markdown
16
+ An object responding thread safely to method `#[]`.
17
+
18
+ A {Server} has no knowledge, and makes no presumptions, about your URI namespace.
19
+ It requires a _Resource Factory_ which produces {Resource Resources} given
20
+ a certain absolute path.
21
+
22
+ The Resource Factory you provide need only implement one method, with signature
23
+ `Resource #[]( String path )`.
24
+ This method will be called with a URI-encoded path string, and must return a
25
+ {Resource}, or `nil` if there's no resource at the given path.
26
+
27
+ For example, if a Rackful client
28
+ tries to access a resource with URI {http://example.com/your/resource http://example.com/some/resource},
29
+ then your Resource Factory can expect to be called like this:
30
+
31
+ resource = resource_factory[ '/your/resource' ]
32
+
33
+ If there's no resource at the given path, but you'd still like to respond to
34
+ `POST` or `PUT` requests to this path, you must return an
35
+ {Resource#empty? empty resource}.
36
+ @return [#[]]
37
+ @see #initialize
38
+ @since 0.0.1
39
+ =end
40
+ attr_reader :resource_factory
41
+
42
+
43
+ =begin markdown
44
+ {include:Server#resource_factory}
45
+ @since 0.0.1
46
+ =end
47
+ def initialize(resource_factory)
48
+ super()
49
+ @resource_factory = resource_factory
50
+ end
51
+
52
+
53
+ =begin markdown
54
+ As required by the Rack specification.
55
+
56
+ For thread safety, this method clones `self`, which handles the request in
57
+ {#call!}. A similar approach is taken by the Sinatra library.
58
+ @return [Array<(status_code, response_headers, response_body)>]
59
+ @since 0.0.1
60
+ =end
61
+ def call(p_env)
62
+ start = Time.now
63
+ retval = dup.call! p_env
64
+ #$stderr.puts( 'Duration: ' + ( Time.now - start ).to_s )
65
+ retval
66
+ end
67
+
68
+
69
+ =begin markdown
70
+ @return [Array<(status_code, response_headers, response_body)>]
71
+ @since 0.0.1
72
+ =end
73
+ def call!(p_env)
74
+ request = Request.new( self.resource_factory, p_env )
75
+ # See also Request::current():
76
+ Thread.current[:rackful_request] = request
77
+ response = Rack::Response.new
78
+ begin
79
+ raise HTTP404NotFound \
80
+ unless resource = self.resource_factory[Path.new(request.path)]
81
+ unless resource.path == request.path
82
+ response.header['Content-Location'] = resource.path
83
+ request.content_path = resource.path
84
+ end
85
+ request.assert_if_headers resource
86
+ if %w{HEAD GET OPTIONS PUT DELETE}.include?( request.request_method )
87
+ resource.__send__( :"http_#{request.request_method}", request, response )
88
+ else
89
+ resource.http_method request, response
90
+ end
91
+ rescue HTTPStatus => e
92
+ # Already handled by HTTPStatus#initialize:
93
+ #raise if $DEBUG && 500 <= e.status
94
+ bct = e.class.best_content_type request.accept, false
95
+ serializer = e.serializer(bct)
96
+ response = Rack::Response.new serializer, e.status, e.headers
97
+ ensure
98
+ # The next line fixes a small peculiarity in RFC2616: the response body of
99
+ # a `HEAD` request _must_ be empty, even for responses outside 2xx.
100
+ if request.head?
101
+ response.body = []
102
+ end
103
+ end
104
+ if 201 == response.status &&
105
+ ( location = response['Location'] ) &&
106
+ ( new_resource = request.resource_factory[location] ) &&
107
+ ! new_resource.empty? \
108
+ or ( (200...300) === response.status ||
109
+ 304 == response.status ) &&
110
+ ! response['Location'] &&
111
+ ( new_resource = request.resource_factory[request.path] ) &&
112
+ ! new_resource.empty?
113
+ response.headers.merge! new_resource.default_headers
114
+ end
115
+ r = response.finish
116
+ $stderr.puts r.inspect
117
+ r
118
+ end
119
+
120
+
121
+ end # class Server
122
+
123
+
124
+ end # module Rackful
data/rackful.gemspec CHANGED
@@ -2,12 +2,14 @@ Gem::Specification.new do |s|
2
2
 
3
3
  # Required properties:
4
4
  s.name = 'rackful'
5
- s.version = '0.0.2'
5
+ s.version = '0.1.0'
6
6
  s.summary = "Library for building ReSTful web services with Rack"
7
7
  s.description = <<EOS
8
8
  Rackful provides a minimal interface for developing ReSTful web services with
9
9
  Rack and Ruby. Instead of writing HTTP method handlers, you'll implement
10
10
  resource objects, which expose their state at URLs.
11
+
12
+ This version is NOT backward compatible with versions 0.0.x.
11
13
  EOS
12
14
  s.files = Dir[ '{example/*,lib/**/*}' ] +
13
15
  %w( rackful.gemspec README.md LICENSE.md mkdoc.sh )
metadata CHANGED
@@ -1,86 +1,83 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: rackful
3
- version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 0
7
- - 0
8
- - 2
9
- version: 0.0.2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
10
6
  platform: ruby
11
- authors:
7
+ authors:
12
8
  - Pieter van Beek
13
9
  autorequire:
14
10
  bindir: bin
15
11
  cert_chain: []
16
-
17
- date: 2012-07-17 00:00:00 +02:00
18
- default_executable:
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
12
+ date: 2012-08-04 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
21
15
  name: rack
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
24
- requirements:
25
- - - ">="
26
- - !ruby/object:Gem::Version
27
- segments:
28
- - 1
29
- - 4
30
- version: "1.4"
16
+ requirement: &70130076580680 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '1.4'
31
22
  type: :runtime
32
- version_requirements: *id001
33
- description: |
34
- Rackful provides a minimal interface for developing ReSTful web services with
35
- Rack and Ruby. Instead of writing HTTP method handlers, you'll implement
23
+ prerelease: false
24
+ version_requirements: *70130076580680
25
+ description: ! 'Rackful provides a minimal interface for developing ReSTful web services
26
+ with
27
+
28
+ Rack and Ruby. Instead of writing HTTP method handlers, you''ll implement
29
+
36
30
  resource objects, which expose their state at URLs.
37
31
 
32
+
33
+ This version is NOT backward compatible with versions 0.0.x.
34
+
35
+ '
38
36
  email: rackful@djinnit.com
39
37
  executables: []
40
-
41
38
  extensions: []
42
-
43
39
  extra_rdoc_files: []
44
-
45
- files:
40
+ files:
46
41
  - example/config.ru
42
+ - example/config2.ru
47
43
  - lib/rackful/header_spoofing.rb
48
44
  - lib/rackful/method_spoofing.rb
49
45
  - lib/rackful/relative_location.rb
50
46
  - lib/rackful.rb
47
+ - lib/rackful_http_status.rb
48
+ - lib/rackful_path.rb
49
+ - lib/rackful_request.rb
50
+ - lib/rackful_resource.rb
51
+ - lib/rackful_serializer.rb
52
+ - lib/rackful_server.rb
51
53
  - rackful.gemspec
52
54
  - README.md
53
55
  - LICENSE.md
54
56
  - mkdoc.sh
55
- has_rdoc: true
56
57
  homepage: http://pieterb.github.com/Rackful/
57
- licenses:
58
+ licenses:
58
59
  - Apache License 2.0
59
60
  post_install_message:
60
61
  rdoc_options: []
61
-
62
- require_paths:
62
+ require_paths:
63
63
  - lib
64
- required_ruby_version: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- segments:
69
- - 0
70
- version: "0"
71
- required_rubygems_version: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- segments:
76
- - 0
77
- version: "0"
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
78
76
  requirements: []
79
-
80
77
  rubyforge_project:
81
- rubygems_version: 1.3.6
78
+ rubygems_version: 1.8.10
82
79
  signing_key:
83
80
  specification_version: 3
84
81
  summary: Library for building ReSTful web services with Rack
85
82
  test_files: []
86
-
83
+ has_rdoc: