rackful 0.1.4 → 0.2.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9219f743c670f4a149826fcf581a97e7c08251d6
4
+ data.tar.gz: 10ec5c1eb9c59f3d7adda6ed9be9518773311ba9
5
+ SHA512:
6
+ metadata.gz: f3d112684686175bc973cd240cce7bf899684fe115184ea94b0cd4797d49aa45a1b939983f5af12f9cc3702a03eb541030023319205996bae5b951711e2fd783
7
+ data.tar.gz: a44bd266844173251e1c0a4ac35c9b7a3be32a77bfd4e369773cb6b49cecc47867e766e04cc8d0bf3eb0f2cbe28779dda4a7b52823b9104ce9e585ad0fec342b
data/CHANGES.md CHANGED
@@ -1,3 +1,14 @@
1
+ 0.2.x
2
+ =====
3
+ The 0.2.x series is a major revision, not backward compatible with 0.1.x.
4
+
5
+ 0.2.0
6
+ -----
7
+ * Removed Request::current, because it won’t work well with single-threaded
8
+ concurrency models (fibers, eventmachine and the like) and it’s not essential.
9
+ * Rackful::Server#call was made reentrant. This is necessary for response code
10
+ 100 Continue.
11
+
1
12
  0.1.x
2
13
  =====
3
14
  The 0.1.x series is a major revision, not backward compatible with 0.0.x.
@@ -9,18 +20,17 @@ The 0.1.x series is a major revision, not backward compatible with 0.0.x.
9
20
 
10
21
  0.1.0
11
22
  -----
12
- * Complete revision of the {Rackful::HTTPStatus} exception class. From now on,
13
- there's
14
- a separate class for each HTTP status code, e.g. {Rackful::HTTP404NotFound}.
15
- * {Rackful::Path}, a subclass of `String`, is used for HTTP paths. This allows
16
- serializers, especially for hypermedia, to distinguish paths from "ordinary"
23
+ * Complete revision of the `Rackful::HTTPStatus` exception class. From now on, there’s
24
+ a separate class for each HTTP status code, e.g. `Rackful::HTTP404NotFound`.
25
+ * `Rackful::Path`, a subclass of `String`, is used for HTTP paths. This allows
26
+ serializers, especially for hypermedia, to distinguish paths from “ordinary”
17
27
  strings, and render them accordingly.
18
- * The concept of {Rackful::Serializer Serializers} was introduced. A serializer
28
+ * The concept of `Rackful::Serializer Serializers` was introduced. A serializer
19
29
  is an object that knows how to serialize an object to a certain media type.
20
30
  * The mechanism for content negotiation has changed completely. See
21
- {Rackful::Resource#serializer} and {Rackful::Resource::ClassMethods#best_content_type}.
31
+ `Rackful::Resource#serializer` and `Rackful::Resource::ClassMethods#best_content_type`.
22
32
  * The mechanism for implementing HTTP method handlers has changed. See
23
- {Rackful::Resource#do_METHOD} and {Rackful::Resource::ClassMethods#add_parser}.
33
+ `Rackful::Resource#do_METHOD` and `Rackful::Resource::ClassMethods#add_parser`.
24
34
 
25
35
  0.0.x
26
36
  =====
data/RACKFUL.md CHANGED
@@ -1,25 +1,27 @@
1
- Rackful
2
- =======
1
+ # @title Rackful
3
2
 
4
- Library for creating Rackful web services
3
+ Library for creating ReSTful web services
5
4
 
6
- The latest documentation is always available
7
- [here, as GitHub pages](http://pieterb.github.com/Rackful/).
5
+ The latest “stable” documentation can be found here:
8
6
 
9
- Rationale
10
- ---------
7
+ * [User documentation](http://pieterb.github.com/Rackful/)
8
+ * [Developer documentation](http://pieterb.github.com/Rackful/devel/) (includes
9
+ documentation for private methods and other internals)
11
10
 
12
- Confronted with the task of implementing a Rackful web service in Ruby, I
11
+ Overview
12
+ ========
13
+
14
+ Confronted with the task of implementing a ReSTful web service in Ruby, I
13
15
  checked out a number of existing libraries and frameworks, including
14
- Ruby-on-Rails, and then decided to brew my own, the reason being that I couldn't
16
+ Ruby-on-Rails, and then decided to brew my own, the reason being that I couldnt
15
17
  find a library or framework with all of the following properties:
16
18
 
17
19
  * **Small** Some of these frameworks are really big. I need to get a job done in
18
20
  time. If understanding the framework takes more time than writing my own, I
19
- must at least feel confident that the framework I'm learning is more powerful
21
+ must at least feel confident that the framework Im learning is more powerful
20
22
  that what I can come up with by myself. Ruby-on-Rails is probably the biggest
21
23
  framework out there, and it still lacks many features that are essential to
22
- Rackful web service programming.
24
+ ReSTful web service programming.
23
25
 
24
26
  This library is small. You could read _all_ the source code in less than an
25
27
  hour, and understand every detail.
@@ -28,20 +30,20 @@ find a library or framework with all of the following properties:
28
30
  subject of more than one flame-war over the years. Not much I can add to the
29
31
  debate...
30
32
 
31
- _...but,_ with Ruby, you shouldn't _need_ code generation!
32
- Unless, of course, you're an ex-Java programmer and/or don't understand
33
+ _...but,_ with Ruby, you shouldnt _need_ code generation!
34
+ Unless, of course, youre an ex-Java programmer and/or dont understand
33
35
  meta-programming.
34
36
 
35
37
  * **Full support for conditional requests** using `If-*:` request headers. Most
36
- libraries' support is limited to `If-None-Match:` and `If-Modified-Since:`
37
- headers, and only for `GET` and `HEAD` requests. For Rackful web services,
38
+ libraries support is limited to `If-None-Match:` and `If-Modified-Since:`
39
+ headers, and only for `GET` and `HEAD` requests. For ReSTful web services,
38
40
  the `If-Match:` and `If-Unmodified-Since:` headers are at least as important,
39
41
  particularly for unsafe methods like `PUT`, `POST`, `PATCH`, and `DELETE`.
40
42
 
41
43
  This library fully supports the `ETag:` and `Last-Modified:` headers, and all
42
44
  `If-*:` headers.
43
45
 
44
- * **Resource centered** Some libraries claim Rackfulness, but at the same
46
+ * **Resource centered** Some libraries claim ReSTfulness, but at the same
45
47
  time have a servet-like interface, which requires you to implement method
46
48
  handles such as `doPOST(url)`. In these method handlers you have to find out
47
49
  what resource is posted to, depending on the URL.
@@ -52,7 +54,7 @@ find a library or framework with all of the following properties:
52
54
  Hello World!
53
55
  ------------
54
56
 
55
- Here's a working example of a simple Rackful server:
57
+ Heres a working example of a simple ReSTful server:
56
58
 
57
59
  {include:file:example/config.ru}
58
60
 
@@ -67,7 +69,7 @@ something like this:
67
69
 
68
70
  Go with your browser to {http://localhost:9292/} and be greeted.
69
71
 
70
- In this example, we implement `GET` and `PUT` requests for the resource at '/'. but
72
+ In this example, we implement `GET` and `PUT` requests for the resource at “/”. but
71
73
  we get a few things for free:
72
74
 
73
75
  ### Free `OPTIONS` response:
@@ -79,36 +81,36 @@ Request:
79
81
 
80
82
  Response:
81
83
 
82
- HTTP/1.1 204 No Content
84
+ HTTP/1.1 204 No Content
83
85
  Allow: PUT, GET, HEAD, OPTIONS
84
86
  Date: Tue, 10 Jul 2012 10:22:52 GMT
85
87
 
86
88
  As you can see, the server accurately reports all available methods for the
87
89
  resource. Notice the availability of the `HEAD` method; if you implement the
88
- `GET` method, you'll get `HEAD` for free. It's still a good idea to explicitly
90
+ `GET` method, youll get `HEAD` for free. Its still a good idea to explicitly
89
91
  implement your own `HEAD` request handler, especially for expensive resources,
90
92
  when responding to a `HEAD` request should be much more efficient than generating
91
93
  a full `GET` response, and strip off the response body.
92
94
 
93
95
  ### Free conditional request handling:
94
96
 
95
- Let's first get the current state of the resource, with this request:
97
+ Lets first get the current state of the resource, with this request:
96
98
 
97
99
  GET / HTTP/1.1
98
100
  Host: localhost:9292
99
101
 
100
102
  Response:
101
103
 
102
- HTTP/1.1 200 OK
104
+ HTTP/1.1 200 OK
103
105
  Content-Type: text/plain
104
106
  Content-Length: 12
105
107
  ETag: "86fb269d190d2c85f6e0468ceca42a20"
106
108
  Date: Tue, 10 Jul 2012 10:34:36 GMT
107
-
109
+
108
110
  Hello world!
109
111
 
110
- Now, we'd like to change the state of the resource, but only if it's still in
111
- the state we last saw, to avoid the "lost update problem". To do that, we
112
+ Now, wed like to change the state of the resource, but only if its still in
113
+ the state we last saw, to avoid the lost update problem”. To do that, we
112
114
  produce an `If-Match:` header, with the entity tag of our last version:
113
115
 
114
116
  PUT / HTTP/1.1
@@ -116,7 +118,7 @@ produce an `If-Match:` header, with the entity tag of our last version:
116
118
  Content-Type: text/plain
117
119
  Content-Length: 31
118
120
  If-Match: "86fb269d190d2c85f6e0468ceca42a20"
119
-
121
+
120
122
  All your base are belong to us.
121
123
 
122
124
  Response:
@@ -131,7 +133,7 @@ resource. When we replay this request, we get the following response:
131
133
  HTTP/1.1 412 Precondition Failed
132
134
  Content-Type: text/html; charset="UTF-8"
133
135
  Date: Tue, 10 Jul 2012 11:06:54 GMT
134
-
136
+
135
137
  [...]
136
138
  <h1>HTTP/1.1 412 Precondition Failed</h1>
137
139
  <p>If-Match: "86fb269d190d2c85f6e0468ceca42a20"</p>
@@ -143,17 +145,27 @@ response body, the server kindly points out exactly which precondition.
143
145
  Further reading
144
146
  ---------------
145
147
  * {Rackful::Server#initialize} for more information about your Resource Factory.
146
- * {Rackful::Resource#get\_etag} and {Rackful::Resource#get\_last\_modified} for more information on
147
- conditional requests.
148
+ * {Rackful::Resource#get\_etag} and {Rackful::Resource#get\_last\_modified} for more
149
+ information on conditional requests.
148
150
  * {Rackful::Resource#do\_METHOD} for more information about writing your own request
149
151
  handlers.
150
- * {Rackful::RelativeLocation} for more information about this piece of Rack middleware
151
- which allows you to return relative and absolute paths in the `Location:`
152
- response header, and why you'd want that.
152
+
153
+ Development
154
+ ===========
155
+ Some conventions used throughout the code and documentation:
156
+
157
+ * In {Rack}, URLs are represented by Ruby Strings. In Rackful, URLs are
158
+ represented by objects of class {::URI::HTTP} wherever possible. This can
159
+ lead to confusion. As a convention, methods and variables that return/contain
160
+ a URL String have `url` in their names. while methods and variables that
161
+ return/contain a {::URI::HTTP} object have `uri` in their names. Yes, it’s
162
+ ugly, but it helps.
163
+ * URI’s are stored and passed around in their {::URI::Generic#normalize normalized}
164
+ form wherever possible.
153
165
 
154
166
  Licensing
155
- ---------
156
- Copyright ©2011-2012 Pieter van Beek <pieterb@sara.nl>
167
+ =========
168
+ Copyright ©2011-2014 Pieter van Beek <pieter@djinnit.com>
157
169
 
158
170
  Licensed under the {file:LICENSE.md Apache License 2.0}. You should have received a copy of the
159
171
  license as part of this distribution.
data/README.md CHANGED
@@ -1,14 +1,20 @@
1
1
  Rackful
2
2
  =======
3
3
 
4
- Library for creating Rackful web services
4
+ Library for creating ReSTful web services
5
5
 
6
- The latest documentation is always available
7
- [here, as GitHub pages](http://pieterb.github.com/Rackful/).
6
+ The latest “stable” documentation can be found here:
7
+
8
+ * [User documentation](http://pieterb.github.com/Rackful/)
9
+ * [Developer documentation](http://pieterb.github.com/Rackful/devel/) (includes
10
+ documentation for private methods and other internals)
11
+
12
+ Documentation for older versions:
13
+ * [User documentation v0.1.5](http://pieterb.github.com/Rackful/0.1.5)
8
14
 
9
15
  Licensing
10
16
  ---------
11
- Copyright ©2011-2012 Pieter van Beek <pieterb@sara.nl>
17
+ Copyright ©2011-2014 Pieter van Beek <pieter@djinnit.com>
12
18
 
13
- Licensed under the Apache License 2.0. You should have received a copy of the
19
+ Licensed under the {file:LICENSE.md Apache License 2.0}. You should have received a copy of the
14
20
  license as part of this distribution.
data/example/config.ru CHANGED
@@ -1,44 +1,38 @@
1
1
  # Load core functionality:
2
2
  require 'rackful'
3
3
 
4
- # Load extra middlewares: ({Rackful::MethodSpoofing}, {Rackful::HeaderSpoofing},
5
- # Rackful::RelativeLocation})
4
+ # Load extra middlewares: ({Rackful::MethodOverride}, {Rackful::HeaderSpoofing})
6
5
  require 'rackful/middleware'
7
6
 
8
7
  require 'digest/md5'
9
8
 
10
9
  # The class of the object we're going to serve:
11
- class Root
12
- include Rackful::Resource
10
+ class Root < Rackful::Resource
13
11
  attr_reader :to_rackful
14
12
  def initialize
15
- self.path = '/'
16
- @to_rackful = 'Hello world!'
13
+ super( 'http://localhost:9292/hallo_wereld' )
14
+ @to_rackful = {
15
+ :a => 'Hello',
16
+ :b => Time.now,
17
+ :c => URI('http://www.example.com/some/path') }
17
18
  end
18
19
  def do_PUT request, response
19
- @to_rackful = request.body.read.encode( Encoding::UTF_8 )
20
+ @to_rackful = self.parser(request).parse
20
21
  end
21
22
  def get_etag
22
- '"' + Digest::MD5.new.update(to_rackful).to_s + '"'
23
+ '"' + Digest::MD5.new.update(to_rackful.inspect).to_s + '"'
23
24
  end
24
- add_serializer Rackful::XHTML, 1.0
25
- add_serializer Rackful::JSON, 1.0
26
- add_media_type 'text/plain'
25
+ add_serializer Rackful::Serializer::XHTML, 1.0
26
+ add_serializer Rackful::Serializer::JSON, 0.5
27
+ add_parser Rackful::Parser::XHTML
28
+ add_parser Rackful::Parser::JSON
27
29
  end
28
- $root_resource = Root.new
30
+ $root_resource = nil
29
31
 
30
- # Rackful::Server needs a resource factory which can map URIs to resource objects:
31
- class ResourceFactory
32
- def [] uri
33
- case uri
34
- when '/'; $root_resource
35
- else; nil
36
- end
37
- end
38
- end
39
-
40
- use Rackful::MethodSpoofing
32
+ use Rack::Reloader
33
+ use Rackful::MethodOverride
41
34
  use Rackful::HeaderSpoofing
42
- use Rackful::RelativeLocation
43
35
 
44
- run Rackful::Server.new ResourceFactory.new
36
+ run Rackful::Server.new { |uri|
37
+ $root_resource ||= Root.new
38
+ }
data/lib/rackful.rb CHANGED
@@ -1,6 +1,15 @@
1
- require 'rackful/path.rb'
1
+ require 'uri'
2
+ require 'nokogiri'
3
+ require 'rack'
4
+ require 'rack/utils'
5
+ require 'base64'
6
+ require 'time'
7
+ require 'json'
8
+
9
+ require 'rackful/uri.rb'
2
10
  require 'rackful/request.rb'
3
11
  require 'rackful/serializer.rb'
12
+ require 'rackful/parser.rb'
4
13
  require 'rackful/resource.rb'
5
- require 'rackful/http_status.rb'
14
+ require 'rackful/httpstatus.rb'
6
15
  require 'rackful/server.rb'
@@ -0,0 +1,250 @@
1
+ # encoding: utf-8
2
+ # Required for parsing:
3
+ require 'rackful/resource.rb'
4
+ require 'rackful/serializer.rb'
5
+
6
+
7
+ module Rackful
8
+
9
+ =begin markdown
10
+ Exception which represents an HTTP Status response.
11
+ @abstract
12
+ =end
13
+ class HTTPStatus < RuntimeError
14
+
15
+
16
+ class Resource < ::Rackful::Resource
17
+ class XHTML < Serializer::XHTML
18
+
19
+
20
+ def header
21
+ retval = super
22
+ retval += "<h1>HTTP/1.1 #{Rack::Utils.escape_html(resource.title)}</h1>\n"
23
+ unless resource.message.empty?
24
+ retval += "<div id=\"rackful-description\">#{resource.message}</div>\n"
25
+ end
26
+ retval
27
+ end
28
+
29
+
30
+ def headers; self.resource.headers; end
31
+
32
+
33
+ end # class Rackful::HTTPStatus::XHTML
34
+
35
+
36
+ add_serializer XHTML, 1.0
37
+
38
+ extend Forwardable
39
+ def_delegators :@http_status_object, :headers, :to_rackful, :title, :message
40
+
41
+
42
+ def initialize(uri, http_status_object)
43
+ super(uri)
44
+ @http_status_object = http_status_object
45
+ end
46
+
47
+
48
+ end # class Rackful::HTTPStatus::Resource
49
+
50
+
51
+ attr_reader :status, :headers, :to_rackful
52
+
53
+
54
+ =begin markdown
55
+ @param status [Symbol, Integer] e.g. `404` or `:not_found`
56
+ @param message [String] XHTML
57
+ @param info [ { Symbol => Object, String => String } ]
58
+ * **Objects** indexed by **Symbols** are returned in the response body.
59
+ * **Strings** indexed by **Strings** are returned as response headers.
60
+ =end
61
+ def initialize status, message = nil, info = {}
62
+ @status = Rack::Utils.status_code status
63
+ raise "Wrong status: #{status}" if 0 === @status
64
+ message ||= ''
65
+ @headers = {}
66
+ @to_rackful = {}
67
+ info.each do
68
+ |k, v|
69
+ if k.kind_of? Symbol
70
+ @to_rackful[k] = v
71
+ else
72
+ @headers[k] = v.to_s
73
+ end
74
+ end
75
+ @to_rackful = nil if @to_rackful.empty?
76
+ if message
77
+ message = message.to_s
78
+ begin
79
+ Nokogiri.XML(
80
+ '<?xml version="1.0" encoding="UTF-8" ?>' +
81
+ "<div>#{message}</div>"
82
+ ) do |config| config.strict.nonet end
83
+ rescue
84
+ message = Rack::Utils.escape_html(message)
85
+ end
86
+ end
87
+ super message
88
+ end
89
+
90
+ def serializer request
91
+ Resource.new( request.url, self ).serializer( request )
92
+ end
93
+
94
+ def title
95
+ "#{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}"
96
+ end
97
+
98
+
99
+
100
+ end # class Rackful::HTTPStatus
101
+
102
+
103
+ =begin markdown
104
+ @abstract Base class for HTTP status codes with only a simple text message, or
105
+ no message at all.
106
+ =end
107
+ class HTTPSimpleStatus < HTTPStatus
108
+
109
+ def initialize message = nil
110
+ /HTTP(\d\d\d)\w+\z/ === self.class.to_s
111
+ status = $1.to_i
112
+ super( status, message )
113
+ end
114
+
115
+ end
116
+
117
+
118
+ class HTTP201Created < HTTPStatus
119
+
120
+ def initialize locations
121
+ locations = [ locations ] unless locations.kind_of? Array
122
+ locations = locations.collect { |l| URI(l) }
123
+ if locations.size > 1
124
+ super( 201, 'New resources were created:', :locations => locations )
125
+ else
126
+ location = locations[0]
127
+ super(
128
+ 201, 'A new resource was created:',
129
+ :"Location" => location, 'Location' => location
130
+ )
131
+ end
132
+ end
133
+
134
+ end
135
+
136
+
137
+ class HTTP202Accepted < HTTPStatus
138
+
139
+ def initialize location = nil
140
+ if location
141
+ location = URI(location)
142
+ super(
143
+ 202, "The request body you sent has been accepted for processing.",
144
+ :"Job status location:" => location, 'Location' => location
145
+ )
146
+ else
147
+ super 202
148
+ end
149
+ end
150
+
151
+ end
152
+
153
+
154
+ class HTTP301MovedPermanently < HTTPStatus
155
+
156
+ def initialize location
157
+ location = URI(location)
158
+ super( 301, '', :'New location:' => location, 'Location' => location )
159
+ end
160
+
161
+ end
162
+
163
+
164
+ class HTTP303SeeOther < HTTPStatus
165
+
166
+ def initialize location
167
+ location = URI(location)
168
+ super( 303, '', :'See:' => location, 'Location' => location )
169
+ end
170
+
171
+ end
172
+
173
+
174
+ class HTTP304NotModified < HTTPStatus
175
+
176
+ def initialize
177
+ super( 304 )
178
+ end
179
+
180
+ end
181
+
182
+
183
+ class HTTP307TemporaryRedirect < HTTPStatus
184
+
185
+ def initialize location
186
+ location = URI(location)
187
+ super( 301, '', :'Current location:' => location, 'Location' => location )
188
+ end
189
+
190
+ end
191
+
192
+
193
+ class HTTP400BadRequest < HTTPSimpleStatus; end
194
+
195
+ class HTTP403Forbidden < HTTPSimpleStatus; end
196
+
197
+ class HTTP404NotFound < HTTPSimpleStatus; end
198
+
199
+
200
+ class HTTP405MethodNotAllowed < HTTPStatus
201
+
202
+ def initialize methods
203
+ super( 405, '', 'Allow' => methods.join(', '), :'Allowed methods:' => methods )
204
+ end
205
+
206
+ end
207
+
208
+
209
+ class HTTP406NotAcceptable < HTTPStatus
210
+
211
+ def initialize content_types
212
+ super( 406, '', :'Available content-type(s):' => content_types )
213
+ end
214
+
215
+ end
216
+
217
+
218
+ class HTTP409Conflict < HTTPSimpleStatus; end
219
+
220
+ class HTTP410Gone < HTTPSimpleStatus; end
221
+
222
+ class HTTP411LengthRequired < HTTPSimpleStatus; end
223
+
224
+
225
+ class HTTP412PreconditionFailed < HTTPStatus
226
+
227
+ def initialize header = nil
228
+ info = header ? { :'Failed precondition:' => header } : {}
229
+ super( 412, '', info )
230
+ end
231
+
232
+ end
233
+
234
+
235
+ class HTTP415UnsupportedMediaType < HTTPStatus
236
+
237
+ def initialize media_types
238
+ super( 415, '', :'Supported media-type(s):' => media_types )
239
+ end
240
+
241
+ end
242
+
243
+
244
+ class HTTP422UnprocessableEntity < HTTPSimpleStatus; end
245
+
246
+ class HTTP501NotImplemented < HTTPSimpleStatus; end
247
+
248
+ class HTTP503ServiceUnavailable < HTTPSimpleStatus; end
249
+
250
+ end # module Rackful