rackful 0.0.2 → 0.1.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,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: