webmachine 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +11 -7
- data/Guardfile +1 -1
- data/README.md +88 -36
- data/Rakefile +10 -0
- data/lib/webmachine/adapters/mongrel.rb +9 -13
- data/lib/webmachine/adapters/rack.rb +50 -0
- data/lib/webmachine/adapters/webrick.rb +2 -0
- data/lib/webmachine/chunked_body.rb +43 -0
- data/lib/webmachine/decision/conneg.rb +2 -1
- data/lib/webmachine/decision/flow.rb +4 -2
- data/lib/webmachine/decision/helpers.rb +4 -0
- data/lib/webmachine/dispatcher.rb +15 -2
- data/lib/webmachine/dispatcher/route.rb +4 -0
- data/lib/webmachine/fiber18.rb +88 -0
- data/lib/webmachine/headers.rb +16 -1
- data/lib/webmachine/media_type.rb +3 -0
- data/lib/webmachine/request.rb +17 -7
- data/lib/webmachine/resource.rb +2 -1
- data/lib/webmachine/resource/authentication.rb +35 -0
- data/lib/webmachine/resource/callbacks.rb +15 -10
- data/lib/webmachine/response.rb +5 -4
- data/lib/webmachine/streaming.rb +44 -8
- data/lib/webmachine/translation.rb +7 -0
- data/lib/webmachine/version.rb +5 -1
- data/spec/tests.org +24 -1
- data/spec/webmachine/adapters/rack_spec.rb +64 -0
- data/spec/webmachine/chunked_body_spec.rb +30 -0
- data/spec/webmachine/decision/helpers_spec.rb +11 -0
- data/spec/webmachine/dispatcher_spec.rb +13 -0
- data/spec/webmachine/request_spec.rb +5 -0
- data/spec/webmachine/resource/authentication_spec.rb +68 -0
- data/webmachine.gemspec +6 -3
- metadata +251 -58
data/Gemfile
CHANGED
@@ -9,13 +9,17 @@ gem 'bundler'
|
|
9
9
|
unless ENV['TRAVIS']
|
10
10
|
gem 'guard-rspec'
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
platform :mri do
|
13
|
+
gem 'redcarpet'
|
14
|
+
|
15
|
+
case RbConfig::CONFIG['host_os']
|
16
|
+
when /darwin/
|
17
|
+
gem 'rb-fsevent'
|
18
|
+
gem 'growl_notify'
|
19
|
+
when /linux/
|
20
|
+
gem 'rb-inotify'
|
21
|
+
gem 'libnotify'
|
22
|
+
end
|
19
23
|
end
|
20
24
|
end
|
21
25
|
|
data/Guardfile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
gemset = ENV['RVM_GEMSET'] || 'webmachine'
|
2
2
|
gemset = "@#{gemset}" unless gemset.to_s == ''
|
3
3
|
|
4
|
-
rvms = %W[
|
4
|
+
rvms = %W[ 1.8.7 1.9.2 jruby rbx ].map {|v| "#{v}#{gemset}" }
|
5
5
|
|
6
6
|
guard 'rspec', :cli => "--color --profile", :growl => true, :rvm => rvms do
|
7
7
|
watch(%r{^lib/webmachine/locale/.+$}) { "spec" }
|
data/README.md
CHANGED
@@ -11,14 +11,14 @@ toolkit for building HTTP-friendly applications. For example, it does
|
|
11
11
|
not provide a templating engine or a persistence layer; those choices
|
12
12
|
are up to you.
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
Webmachine
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
14
|
+
## A Note about Rack
|
15
|
+
|
16
|
+
Webmachine has a Rack adapter -- thanks to Jamis Buck -- but when
|
17
|
+
using it, we recommend you ensure that NO middleware is used. The
|
18
|
+
behaviors that are encapsulated in Webmachine could be broken by
|
19
|
+
middlewares that sit above it, and there is no way to detect them at
|
20
|
+
runtime. _Caveat emptor_. That said, Webmachine should behave properly
|
21
|
+
when given a clear stack.
|
22
22
|
|
23
23
|
## Getting Started
|
24
24
|
|
@@ -26,25 +26,25 @@ Webmachine is very young, but it's still easy to construct an
|
|
26
26
|
application for it!
|
27
27
|
|
28
28
|
```ruby
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
29
|
+
require 'webmachine'
|
30
|
+
# Require any of the files that contain your resources here
|
31
|
+
require 'my_resource'
|
32
|
+
|
33
|
+
# Point all URIs at the MyResource class
|
34
|
+
Webmachine::Dispatcher.add_route(['*'], MyResource)
|
35
|
+
|
36
|
+
# Start the server, binds to port 8080 using WEBrick
|
37
|
+
Webmachine.run
|
38
38
|
```
|
39
39
|
|
40
40
|
Your resource will look something like this:
|
41
41
|
|
42
42
|
```ruby
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
43
|
+
class MyResource < Webmachine::Resource
|
44
|
+
def to_html
|
45
|
+
"<html><body>Hello, world!</body></html>"
|
46
|
+
end
|
47
|
+
end
|
48
48
|
```
|
49
49
|
|
50
50
|
Run the first file and your application is up. That's all there is to
|
@@ -54,19 +54,44 @@ might want to enable "gzip" compression on your resource, for which
|
|
54
54
|
you can simply add an `encodings_provided` callback method:
|
55
55
|
|
56
56
|
```ruby
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
57
|
+
class MyResource < Webmachine::Resource
|
58
|
+
def encodings_provided
|
59
|
+
{"gzip" => :encode_gzip, "identity" => :encode_identity}
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_html
|
63
|
+
"<html><body>Hello, world!</body></html>"
|
64
|
+
end
|
65
|
+
end
|
66
66
|
```
|
67
67
|
|
68
68
|
There are many other HTTP features exposed to your resource through
|
69
|
-
|
69
|
+
{Webmachine::Resource::Callbacks}. Give them a try!
|
70
|
+
|
71
|
+
### Configurator
|
72
|
+
|
73
|
+
There's a configurator that allows you to set the ip address and port
|
74
|
+
bindings as well as a different webserver adapter. You can also add
|
75
|
+
your routes in a block. Both of these call return the `Webmachine`
|
76
|
+
module, so you could chain them if you like.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
require 'webmachine'
|
80
|
+
require 'my_resource'
|
81
|
+
|
82
|
+
Webmachine.routes do
|
83
|
+
add ['*'], MyResource
|
84
|
+
end
|
85
|
+
|
86
|
+
Webmachine.configure do |config|
|
87
|
+
config.ip = '127.0.0.1'
|
88
|
+
config.port = 3000
|
89
|
+
config.adapter = :Mongrel
|
90
|
+
end
|
91
|
+
|
92
|
+
# Start the server.
|
93
|
+
Webmachine.run
|
94
|
+
```
|
70
95
|
|
71
96
|
## Features
|
72
97
|
|
@@ -75,14 +100,14 @@ callbacks. Give them a try!
|
|
75
100
|
* Most callbacks can interrupt the decision flow by returning an
|
76
101
|
integer response code. You generally only want to do this when new
|
77
102
|
information comes to light, requiring a modification of the response.
|
78
|
-
* Supports WEBrick and Mongrel (1.2pre+). Other host
|
79
|
-
investigated.
|
80
|
-
* Streaming/chunked response bodies are permitted as Enumerables
|
103
|
+
* Supports WEBrick and Mongrel (1.2pre+), and a Rack shim. Other host
|
104
|
+
servers are being investigated.
|
105
|
+
* Streaming/chunked response bodies are permitted as Enumerables,
|
106
|
+
Procs, or Fibers!
|
81
107
|
* Unlike the Erlang original, it does real Language negotiation.
|
82
108
|
|
83
109
|
## Problems/TODOs
|
84
110
|
|
85
|
-
* Support streamed responses as Fibers.
|
86
111
|
* Command-line tools, and general polish.
|
87
112
|
* Tracing is exposed as an Array of decisions visited on the response
|
88
113
|
object. You should be able to turn this off and on, and visualize
|
@@ -90,6 +115,33 @@ callbacks. Give them a try!
|
|
90
115
|
|
91
116
|
## Changelog
|
92
117
|
|
118
|
+
### 0.3.0 November 9, 2011
|
119
|
+
|
120
|
+
0.3.0 introduces some new features, refactorings, and now has 100%
|
121
|
+
documentation coverage! Among the new features are minimal Rack
|
122
|
+
compatibility, streaming responses via Fibers and a friendlier route
|
123
|
+
definition syntax. Added Jamis Buck as a committer. Thank you for your
|
124
|
+
contributions!
|
125
|
+
|
126
|
+
* Chunked bodies are now wrapped in a way that works on webservers
|
127
|
+
that don't automatically produce them.
|
128
|
+
* HTTP Basic Authentication is easy to add to resources, just include
|
129
|
+
`Webmachine::Resource::Authentication`.
|
130
|
+
* Routes are a little less painful to add, you can now specify them
|
131
|
+
with `Webmachine.routes` which will be evaled into the `Dispatcher`.
|
132
|
+
* The new default port is 8080.
|
133
|
+
* Rack is minimally supported as a host server. _Don't put middleware
|
134
|
+
above Webmachine!_
|
135
|
+
* Fibers can be used as streamed response bodies.
|
136
|
+
* `Dispatcher#add_route` will now return the added `Route` instance.
|
137
|
+
* The header-conversion code for CGI-style servers has been extracted
|
138
|
+
into `Webmachine::Headers`.
|
139
|
+
* `Route#path_spec` is now public so that applications can inspect
|
140
|
+
existing routes, perhaps for URL generation.
|
141
|
+
* `Request#query` now uses `CGI.unescape` so '+' characters are
|
142
|
+
correctly parsed.
|
143
|
+
* YARD documentation has 100% coverage.
|
144
|
+
|
93
145
|
### 0.2.0 September 11, 2011
|
94
146
|
|
95
147
|
0.2.0 includes an adapter for Mongrel and a central place for
|
data/Rakefile
CHANGED
@@ -1,6 +1,16 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rubygems/package_task'
|
3
3
|
|
4
|
+
begin
|
5
|
+
require 'yard'
|
6
|
+
require 'yard/rake/yardoc_task'
|
7
|
+
YARD::Rake::YardocTask.new do |doc|
|
8
|
+
doc.files = Dir["lib/**/*.rb"] + ['README.md']
|
9
|
+
doc.options = ["-m", "markdown"]
|
10
|
+
end
|
11
|
+
rescue LoadError
|
12
|
+
end
|
13
|
+
|
4
14
|
def gemspec
|
5
15
|
$webmachine_gemspec ||= Gem::Specification.load("webmachine.gemspec")
|
6
16
|
end
|
@@ -4,6 +4,7 @@ require 'webmachine/headers'
|
|
4
4
|
require 'webmachine/request'
|
5
5
|
require 'webmachine/response'
|
6
6
|
require 'webmachine/dispatcher'
|
7
|
+
require 'webmachine/chunked_body'
|
7
8
|
|
8
9
|
module Webmachine
|
9
10
|
module Adapters
|
@@ -26,9 +27,11 @@ module Webmachine
|
|
26
27
|
config.join
|
27
28
|
end
|
28
29
|
|
30
|
+
# A Mongrel handler for Webmachine
|
29
31
|
class Handler < ::Mongrel::HttpHandler
|
32
|
+
# Processes an individual request from Mongrel through Webmachine.
|
30
33
|
def process(wreq, wres)
|
31
|
-
header =
|
34
|
+
header = Webmachine::Headers.from_cgi(wreq.params)
|
32
35
|
|
33
36
|
request = Webmachine::Request.new(wreq.params["REQUEST_METHOD"],
|
34
37
|
URI.parse(wreq.params["REQUEST_URI"]),
|
@@ -55,29 +58,22 @@ module Webmachine
|
|
55
58
|
wres.write response.body
|
56
59
|
wres.socket.flush
|
57
60
|
when Enumerable
|
58
|
-
response.body.each { |part|
|
61
|
+
Webmachine::ChunkedBody.new(response.body).each { |part|
|
59
62
|
wres.write part
|
60
63
|
wres.socket.flush
|
61
64
|
}
|
62
65
|
else
|
63
66
|
if response.body.respond_to?(:call)
|
64
|
-
|
65
|
-
|
67
|
+
Webmachine::ChunkedBody.new(Array(response.body.call)).each { |part|
|
68
|
+
wres.write part
|
69
|
+
wres.socket.flush
|
70
|
+
}
|
66
71
|
end
|
67
72
|
end
|
68
73
|
ensure
|
69
74
|
response.body.close if response.body.respond_to? :close
|
70
75
|
end
|
71
76
|
end
|
72
|
-
|
73
|
-
def http_headers(env, headers)
|
74
|
-
env.inject(headers) do |h,(k,v)|
|
75
|
-
if k =~ /^HTTP_(\w+)$/
|
76
|
-
h[$1.tr("_", "-")] = v
|
77
|
-
end
|
78
|
-
h
|
79
|
-
end
|
80
|
-
end
|
81
77
|
end
|
82
78
|
end
|
83
79
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rack/request'
|
2
|
+
require 'webmachine/version'
|
3
|
+
require 'webmachine/headers'
|
4
|
+
require 'webmachine/request'
|
5
|
+
require 'webmachine/response'
|
6
|
+
require 'webmachine/dispatcher'
|
7
|
+
|
8
|
+
module Webmachine
|
9
|
+
module Adapters
|
10
|
+
# A minimal "shim" adapter to allow Webmachine to interface with Rack. The
|
11
|
+
# intention here is to allow Webmachine to run under Rack-compatible
|
12
|
+
# web-servers, like unicorn and pow, and is not intended to allow Webmachine
|
13
|
+
# to be "plugged in" to an existing Rack app as middleware.
|
14
|
+
#
|
15
|
+
# To use this adapter, create a config.ru file and populate it like so:
|
16
|
+
#
|
17
|
+
# require 'webmachine/adapters/rack'
|
18
|
+
#
|
19
|
+
# # put your own Webmachine resources in another file:
|
20
|
+
# require 'my/resources'
|
21
|
+
#
|
22
|
+
# run Webmachine::Adapters::Rack.new
|
23
|
+
#
|
24
|
+
# Servers like pow and unicorn will read config.ru by default and it should
|
25
|
+
# all "just work".
|
26
|
+
class Rack
|
27
|
+
# Handles a Rack-based request.
|
28
|
+
# @param [Hash] env the Rack environment
|
29
|
+
def call(env)
|
30
|
+
headers = Webmachine::Headers.from_cgi(env)
|
31
|
+
|
32
|
+
rack_req = ::Rack::Request.new env
|
33
|
+
request = Webmachine::Request.new(rack_req.request_method,
|
34
|
+
URI.parse(rack_req.url),
|
35
|
+
headers,
|
36
|
+
rack_req.body)
|
37
|
+
|
38
|
+
response = Webmachine::Response.new
|
39
|
+
Webmachine::Dispatcher.dispatch request, response
|
40
|
+
|
41
|
+
response.headers['Server'] = [Webmachine::SERVER_STRING, "Rack/#{::Rack.version}"].join(" ")
|
42
|
+
|
43
|
+
body = response.body.respond_to?(:call) ? response.body.call : response.body
|
44
|
+
body = body.is_a?(String) ? [ body ] : body
|
45
|
+
|
46
|
+
[response.code.to_i, response.headers, body || []]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -21,7 +21,9 @@ module Webmachine
|
|
21
21
|
Thread.new { server.start }.join
|
22
22
|
end
|
23
23
|
|
24
|
+
# WEBRick::HTTPServer that is run by the WEBrick adapter.
|
24
25
|
class Server < ::WEBrick::HTTPServer
|
26
|
+
# Handles a request
|
25
27
|
def service(wreq, wres)
|
26
28
|
header = Webmachine::Headers.new
|
27
29
|
wreq.each {|k,v| header[k] = v }
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Webmachine
|
2
|
+
# {ChunkedBody} is used to wrap an {Enumerable} object (like an enumerable
|
3
|
+
# {Response#body}) so it yields proper chunks for chunked transfer encoding.
|
4
|
+
#
|
5
|
+
# case response.body
|
6
|
+
# when String
|
7
|
+
# socket.write(response.body)
|
8
|
+
# when Enumerable
|
9
|
+
# Webmachine::ChunkedBody.new(response.body).each do |chunk|
|
10
|
+
# socket.write(chunk)
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# This is needed for Ruby webservers which don't do the chunking themselves.
|
15
|
+
class ChunkedBody
|
16
|
+
# Delimiter for chunked encoding
|
17
|
+
CRLF = "\r\n"
|
18
|
+
|
19
|
+
# Final chunk in any chunked-encoding response
|
20
|
+
FINAL_CHUNK = "0#{CRLF}#{CRLF}"
|
21
|
+
|
22
|
+
# Creates a new {ChunkedBody} from the given {Enumerable}.
|
23
|
+
# @param [Enumerable] body the enumerable response body
|
24
|
+
# @return [ChunkedBody] the wrapped response body
|
25
|
+
def initialize(body)
|
26
|
+
@body = body
|
27
|
+
end
|
28
|
+
|
29
|
+
# Calls the given block once for each chunk, passing that chunk as a
|
30
|
+
# parameter.
|
31
|
+
# Returns an {Enumerator} if no block is given.
|
32
|
+
def each
|
33
|
+
return self.to_enum unless block_given?
|
34
|
+
|
35
|
+
@body.each do |chunk|
|
36
|
+
size = chunk.bytesize
|
37
|
+
next if size == 0
|
38
|
+
yield([size.to_s(16), CRLF, chunk, CRLF].join)
|
39
|
+
end
|
40
|
+
yield(FINAL_CHUNK)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -87,6 +87,7 @@ module Webmachine
|
|
87
87
|
# equals the tag, or if it exactly equals a prefix of the
|
88
88
|
# tag such that the first tag character following the prefix
|
89
89
|
# is "-".
|
90
|
+
# @api private
|
90
91
|
def language_match(range, tag)
|
91
92
|
range.downcase == tag.downcase || tag =~ /^#{Regexp.escape(range)}\-/i
|
92
93
|
end
|
@@ -215,7 +216,7 @@ module Webmachine
|
|
215
216
|
end
|
216
217
|
end
|
217
218
|
|
218
|
-
# Like a {PriorityList}, but for {
|
219
|
+
# Like a {PriorityList}, but for {MediaType}s, since they have
|
219
220
|
# parameters in addition to q.
|
220
221
|
# @private
|
221
222
|
class MediaTypeList < PriorityList
|
@@ -6,8 +6,9 @@ require 'webmachine/translation'
|
|
6
6
|
module Webmachine
|
7
7
|
module Decision
|
8
8
|
# This module encapsulates all of the decisions in Webmachine's
|
9
|
-
# flow-chart. These invoke {Resource
|
10
|
-
# appropriate response code, headers, and body for
|
9
|
+
# flow-chart. These invoke {Webmachine::Resource::Callbacks} methods to
|
10
|
+
# determine the appropriate response code, headers, and body for
|
11
|
+
# the response.
|
11
12
|
#
|
12
13
|
# This module is included into {FSM}, which drives the processing
|
13
14
|
# of the chart.
|
@@ -390,6 +391,7 @@ module Webmachine
|
|
390
391
|
decision_test(resource.delete_resource, true, :m20b, 500)
|
391
392
|
end
|
392
393
|
|
394
|
+
# Did the DELETE complete?
|
393
395
|
def m20b
|
394
396
|
decision_test(resource.delete_completed?, true, :o20, 202)
|
395
397
|
end
|
@@ -5,6 +5,7 @@ module Webmachine
|
|
5
5
|
module Decision
|
6
6
|
# Methods that assist the Decision {Flow}.
|
7
7
|
module Helpers
|
8
|
+
# Pattern for quoted headers
|
8
9
|
QUOTED = /^"(.*)"$/
|
9
10
|
|
10
11
|
# Determines if the response has a body/entity set.
|
@@ -28,6 +29,8 @@ module Webmachine
|
|
28
29
|
response.body = case body
|
29
30
|
when String # 1.8 treats Strings as Enumerable
|
30
31
|
resource.send(encoder, resource.send(charsetter, body))
|
32
|
+
when Fiber
|
33
|
+
FiberEncoder.new(resource, encoder, charsetter, body)
|
31
34
|
when Enumerable
|
32
35
|
EnumerableEncoder.new(resource, encoder, charsetter, body)
|
33
36
|
else
|
@@ -84,6 +87,7 @@ module Webmachine
|
|
84
87
|
end
|
85
88
|
end
|
86
89
|
|
90
|
+
# Adds caching-related headers to the response.
|
87
91
|
def add_caching_headers
|
88
92
|
if etag = resource.generate_etag
|
89
93
|
response.headers['ETag'] = ensure_quoted_header(etag)
|