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
@@ -12,9 +12,12 @@ module Webmachine
|
|
12
12
|
# order they are added.
|
13
13
|
# @see Route#new
|
14
14
|
def add_route(*args)
|
15
|
-
|
15
|
+
route = Route.new(*args)
|
16
|
+
@routes << route
|
17
|
+
route
|
16
18
|
end
|
17
|
-
|
19
|
+
alias :add :add_route
|
20
|
+
|
18
21
|
# Dispatches a request to the appropriate {Resource} in the
|
19
22
|
# dispatch list. If a matching resource is not found, a "404 Not
|
20
23
|
# Found" will be rendered.
|
@@ -37,4 +40,14 @@ module Webmachine
|
|
37
40
|
@routes = []
|
38
41
|
end
|
39
42
|
end
|
43
|
+
|
44
|
+
# Evaluates the passed block in the context of
|
45
|
+
# {Webmachine::Dispatcher} for use in adding a number of routes at
|
46
|
+
# once.
|
47
|
+
# @return [Webmachine] self
|
48
|
+
# @see Webmachine::Dispatcher.add_route
|
49
|
+
def self.routes(&block)
|
50
|
+
Dispatcher.module_eval(&block)
|
51
|
+
self
|
52
|
+
end
|
40
53
|
end
|
@@ -10,6 +10,10 @@ module Webmachine
|
|
10
10
|
# subclass of {Resource}
|
11
11
|
attr_reader :resource
|
12
12
|
|
13
|
+
# @return [Array<String|Symbol>] the list of path segments
|
14
|
+
# used to define this route (see #initialize).
|
15
|
+
attr_reader :path_spec
|
16
|
+
|
13
17
|
# When used in a path specification, will match all remaining
|
14
18
|
# segments
|
15
19
|
MATCH_ALL = '*'.freeze
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# Poor Man's Fiber (API compatible Thread based Fiber implementation for Ruby 1.8)
|
3
|
+
# (c) 2008 Aman Gupta (tmm1)
|
4
|
+
|
5
|
+
unless defined? Fiber
|
6
|
+
require 'thread'
|
7
|
+
|
8
|
+
# Raised by {Fiber} when they are used improperly
|
9
|
+
class FiberError < StandardError; end
|
10
|
+
|
11
|
+
# Implements a reasonably-compatible Fiber class that can be used on
|
12
|
+
# Rubies that have 1.8-style APIs.
|
13
|
+
class Fiber
|
14
|
+
# @yield the block that should be executed inside the Fiber
|
15
|
+
def initialize
|
16
|
+
raise ArgumentError, 'new Fiber requires a block' unless block_given?
|
17
|
+
|
18
|
+
@yield = Queue.new
|
19
|
+
@resume = Queue.new
|
20
|
+
|
21
|
+
@thread = Thread.new{ @yield.push [ *yield(*@resume.pop) ] }
|
22
|
+
@thread.abort_on_exception = true
|
23
|
+
@thread[:fiber] = self
|
24
|
+
end
|
25
|
+
attr_reader :thread
|
26
|
+
|
27
|
+
# Returns true if the fiber can still be resumed (or transferred
|
28
|
+
# to). After finishing execution of the fiber block this method
|
29
|
+
# will always return false.
|
30
|
+
def alive?
|
31
|
+
@thread.alive?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Resumes the fiber from the point at which the last Fiber.yield
|
35
|
+
# was called, or starts running it if it is the first call to
|
36
|
+
# resume. Arguments passed to resume will be the value of the
|
37
|
+
# Fiber.yield expression or will be passed as block parameters to
|
38
|
+
# the fiber’s block if this is the first resume.
|
39
|
+
#
|
40
|
+
# Alternatively, when resume is called it evaluates to the arguments
|
41
|
+
# passed to the next Fiber.yield statement inside the fiber’s block or
|
42
|
+
# to the block value if it runs to completion without any Fiber.yield
|
43
|
+
def resume *args
|
44
|
+
raise FiberError, 'dead fiber called' unless @thread.alive?
|
45
|
+
@resume.push(args)
|
46
|
+
result = @yield.pop
|
47
|
+
result.size > 1 ? result : result.first
|
48
|
+
end
|
49
|
+
|
50
|
+
# Yields control back to the context that resumed this fiber,
|
51
|
+
# passing along any arguments that were passed to it. The fiber
|
52
|
+
# will resume processing at this point when resume is called
|
53
|
+
# next. Any arguments passed to the next resume will be the value
|
54
|
+
# that this Fiber.yield expression evaluates to.
|
55
|
+
# @note This method is only called internally. In your code, use
|
56
|
+
# {Fiber.yield}.
|
57
|
+
def yield *args
|
58
|
+
@yield.push(args)
|
59
|
+
result = @resume.pop
|
60
|
+
result.size > 1 ? result : result.first
|
61
|
+
end
|
62
|
+
|
63
|
+
# Yields control back to the context that resumed the fiber,
|
64
|
+
# passing along any arguments that were passed to it. The fiber
|
65
|
+
# will resume processing at this point when resume is called
|
66
|
+
# next. Any arguments passed to the next resume will be the value
|
67
|
+
# that this Fiber.yield expression evaluates to. This will raise
|
68
|
+
# a {FiberError} if you are not inside a {Fiber}.
|
69
|
+
# @raise FiberError
|
70
|
+
def self.yield *args
|
71
|
+
raise FiberError, "can't yield from root fiber" unless fiber = Thread.current[:fiber]
|
72
|
+
fiber.yield(*args)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the current fiber. If you are not running in the
|
76
|
+
# context of a fiber this method will raise a {FiberError}.
|
77
|
+
# @raise FiberError
|
78
|
+
def self.current
|
79
|
+
Thread.current[:fiber] or raise FiberError, 'not inside a fiber'
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns a string containing a human-readable representation of
|
83
|
+
# this Fiber.
|
84
|
+
def inspect
|
85
|
+
"#<#{self.class}:0x#{self.object_id.to_s(16)}>"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/webmachine/headers.rb
CHANGED
@@ -1,18 +1,33 @@
|
|
1
1
|
module Webmachine
|
2
2
|
# Case-insensitive Hash of Request headers
|
3
3
|
class Headers < ::Hash
|
4
|
+
# Convert CGI-style Hash into Request headers
|
5
|
+
# @param [Hash] env a hash of CGI-style env/headers
|
6
|
+
def self.from_cgi(env)
|
7
|
+
env.inject(new) do |h,(k,v)|
|
8
|
+
if k =~ /^HTTP_(\w+)$/
|
9
|
+
h[$1.tr("_", "-")] = v
|
10
|
+
end
|
11
|
+
h
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Fetch a header
|
4
16
|
def [](key)
|
5
17
|
super transform_key(key)
|
6
18
|
end
|
7
19
|
|
20
|
+
# Set a header
|
8
21
|
def []=(key,value)
|
9
22
|
super transform_key(key), value
|
10
23
|
end
|
11
24
|
|
25
|
+
# Delete a header
|
12
26
|
def delete(key)
|
13
27
|
super transform_key(key)
|
14
28
|
end
|
15
|
-
|
29
|
+
|
30
|
+
# Select matching headers
|
16
31
|
def grep(pattern)
|
17
32
|
self.class[select { |k,_| pattern === k }]
|
18
33
|
end
|
@@ -51,6 +51,9 @@ module Webmachine
|
|
51
51
|
@type == "*/*" && @params.empty?
|
52
52
|
end
|
53
53
|
|
54
|
+
# @return [true,false] Are these two types strictly equal?
|
55
|
+
# @param other the other media type.
|
56
|
+
# @see MediaType.parse
|
54
57
|
def ==(other)
|
55
58
|
other = self.class.parse(other)
|
56
59
|
other.type == type && other.params == params
|
data/lib/webmachine/request.rb
CHANGED
@@ -1,20 +1,30 @@
|
|
1
|
+
require 'cgi'
|
1
2
|
require 'forwardable'
|
2
3
|
|
3
4
|
module Webmachine
|
4
|
-
#
|
5
|
+
# Request represents a single HTTP request sent from a client. It
|
6
|
+
# should be instantiated by {Adapters} when a request is received
|
5
7
|
class Request
|
6
8
|
extend Forwardable
|
7
9
|
attr_reader :method, :uri, :headers, :body
|
8
10
|
attr_accessor :disp_path, :path_info, :path_tokens
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
+
# @param [String] method the HTTP request method
|
13
|
+
# @param [URI] uri the requested URI, including host, scheme and
|
14
|
+
# port
|
15
|
+
# @param [Headers] headers the HTTP request headers
|
16
|
+
# @param [String,#to_s,#each,nil] body the entity included in the
|
17
|
+
# request, if present
|
18
|
+
def initialize(method, uri, headers, body)
|
19
|
+
@method, @uri, @headers, @body = method, uri, headers, body
|
12
20
|
end
|
13
21
|
|
14
22
|
def_delegators :headers, :[]
|
15
23
|
|
16
|
-
#
|
17
|
-
|
24
|
+
# Enables quicker access to request headers by using a
|
25
|
+
# lowercased-underscored version of the header name, e.g.
|
26
|
+
# `if_unmodified_since`.
|
27
|
+
def method_missing(m, *args, &block)
|
18
28
|
if m.to_s =~ /^(?:[a-z0-9])+(?:_[a-z0-9]+)*$/i
|
19
29
|
# Access headers more easily as underscored methods.
|
20
30
|
self[m.to_s.tr('_', '-')]
|
@@ -23,7 +33,7 @@ module Webmachine
|
|
23
33
|
end
|
24
34
|
end
|
25
35
|
|
26
|
-
# Whether the request body is present.
|
36
|
+
# @return[true, false] Whether the request body is present.
|
27
37
|
def has_body?
|
28
38
|
!(body.nil? || body.empty?)
|
29
39
|
end
|
@@ -46,7 +56,7 @@ module Webmachine
|
|
46
56
|
unless @query
|
47
57
|
@query = {}
|
48
58
|
uri.query.split(/&/).each do |kv|
|
49
|
-
k, v =
|
59
|
+
k, v = CGI.unescape(kv).split(/=/)
|
50
60
|
@query[k] = v if k && v
|
51
61
|
end
|
52
62
|
end
|
data/lib/webmachine/resource.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'webmachine/resource/callbacks'
|
2
2
|
require 'webmachine/resource/encodings'
|
3
|
+
require 'webmachine/resource/authentication'
|
3
4
|
|
4
5
|
module Webmachine
|
5
6
|
# Resource is the primary building block of Webmachine applications,
|
@@ -24,7 +25,7 @@ module Webmachine
|
|
24
25
|
attr_reader :request, :response
|
25
26
|
|
26
27
|
# Creates a new {Resource}, initializing it with the request and
|
27
|
-
# response. Note that you may still override
|
28
|
+
# response. Note that you may still override the `initialize` method to
|
28
29
|
# initialize your resource. It will be called after the request
|
29
30
|
# and response ivars are set.
|
30
31
|
# @param [Request] request the request object
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Webmachine
|
2
|
+
class Resource
|
3
|
+
# Helper methods that can be included in your
|
4
|
+
# {Webmachine::Resource} to assist in performing HTTP
|
5
|
+
# Authentication.
|
6
|
+
module Authentication
|
7
|
+
# Pattern for matching Authorization headers that use the Basic
|
8
|
+
# auth scheme.
|
9
|
+
BASIC_HEADER = /^Basic (.*)$/i.freeze
|
10
|
+
|
11
|
+
# A simple implementation of HTTP Basic auth. Call this from the
|
12
|
+
# {Webmachine::Resource::Callbacks#is_authorized?} callback,
|
13
|
+
# giving it a block which will be yielded the username and
|
14
|
+
# password and return true or false.
|
15
|
+
# @param [String] header the value of the Authentication request
|
16
|
+
# header, passed to the {Callbacks#is_authorized?} callback.
|
17
|
+
# @param [String] realm the "realm", or description of the
|
18
|
+
# resource that requires authentication
|
19
|
+
# @return [true, String] true if the client is authorized, or
|
20
|
+
# the appropriate WWW-Authenticate header
|
21
|
+
# @yield [user, password] a block that will verify the client-provided user/password
|
22
|
+
# against application constraints
|
23
|
+
# @yieldparam [String] user the passed username
|
24
|
+
# @yieldparam [String] password the passed password
|
25
|
+
# @yieldreturn [true,false] whether the username/password is correct
|
26
|
+
def basic_auth(header, realm="Webmachine")
|
27
|
+
if header =~ BASIC_HEADER && (yield *$1.unpack('m*').first.split(/:/,2))
|
28
|
+
true
|
29
|
+
else
|
30
|
+
%Q[Basic realm="#{realm}"]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,6 +1,10 @@
|
|
1
1
|
module Webmachine
|
2
2
|
class Resource
|
3
|
-
# These
|
3
|
+
# These methods are the primary way your {Webmachine::Resource}
|
4
|
+
# instance interacts with HTTP and the
|
5
|
+
# {Webmachine::Decision::FSM}. Implementing a callback can change
|
6
|
+
# the portions of the graph that are made available to your
|
7
|
+
# application.
|
4
8
|
module Callbacks
|
5
9
|
# Does the resource exist? Returning a falsey value (false or nil)
|
6
10
|
# will result in a '404 Not Found' response. Defaults to true.
|
@@ -195,12 +199,13 @@ module Webmachine
|
|
195
199
|
|
196
200
|
# This should return an array of pairs where each pair is of the
|
197
201
|
# form [mediatype, :handler] where mediatype is a String of
|
198
|
-
# Content-Type format and :handler
|
199
|
-
# which can provide a resource
|
200
|
-
# type. For example, if a client
|
201
|
-
# header with a value that does not
|
202
|
-
# any of the return pairs, then a
|
203
|
-
# sent. Default is [['text/html',
|
202
|
+
# Content-Type format (or {Webmachine::MediaType}) and :handler
|
203
|
+
# is a Symbol naming the method which can provide a resource
|
204
|
+
# representation in that media type. For example, if a client
|
205
|
+
# request includes an 'Accept' header with a value that does not
|
206
|
+
# appear as a first element in any of the return pairs, then a
|
207
|
+
# '406 Not Acceptable' will be sent. Default is [['text/html',
|
208
|
+
# :to_html]].
|
204
209
|
# @return an array of mediatype/handler pairs
|
205
210
|
# @api callback
|
206
211
|
def content_types_provided
|
@@ -233,9 +238,9 @@ module Webmachine
|
|
233
238
|
# This should return a list of language tags provided by the
|
234
239
|
# resource. Default is the empty Array, in which the content is
|
235
240
|
# in no specific language.
|
236
|
-
# @return [Array<String>] a list of provided languages
|
241
|
+
# @return [Array<String>] a list of provided languages
|
237
242
|
# @api callback
|
238
|
-
def languages_provided
|
243
|
+
def languages_provided
|
239
244
|
[]
|
240
245
|
end
|
241
246
|
|
@@ -247,7 +252,7 @@ module Webmachine
|
|
247
252
|
def language_chosen(lang)
|
248
253
|
@language = lang
|
249
254
|
end
|
250
|
-
|
255
|
+
|
251
256
|
# This should return a hash of encodings mapped to encoding
|
252
257
|
# methods for Content-Encodings your resource wants to
|
253
258
|
# provide. The encoding will be applied to the response body
|
data/lib/webmachine/response.rb
CHANGED
@@ -33,10 +33,11 @@ module Webmachine
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# Indicate that the response should be a redirect. This is only
|
36
|
-
# used when processing a POST request in
|
37
|
-
# to indicate that the client
|
38
|
-
# using GET. Either pass the URI
|
39
|
-
# manually set the Location header
|
36
|
+
# used when processing a POST request in
|
37
|
+
# {Resource::Callbacks#process_post} to indicate that the client
|
38
|
+
# should request another resource using GET. Either pass the URI
|
39
|
+
# of the target resource, or manually set the Location header
|
40
|
+
# using {#headers}.
|
40
41
|
# @param [String, URI] location the target of the redirection
|
41
42
|
def do_redirect(location=nil)
|
42
43
|
headers['Location'] = location.to_s if location
|
data/lib/webmachine/streaming.rb
CHANGED
@@ -1,27 +1,63 @@
|
|
1
|
+
begin
|
2
|
+
require 'fiber'
|
3
|
+
rescue LoadError
|
4
|
+
require 'webmachine/fiber18'
|
5
|
+
end
|
6
|
+
|
1
7
|
module Webmachine
|
2
|
-
class
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
8
|
+
# Subclasses of this class implement means for streamed/chunked
|
9
|
+
# response bodies to be coerced to the negotiated character set and
|
10
|
+
# encoded automatically as they are output to the client.
|
11
|
+
# @api private
|
12
|
+
StreamingEncoder = Struct.new(:resource, :encoder, :charsetter, :body)
|
7
13
|
|
14
|
+
# Implements a streaming encoder for Enumerable response bodies, such as
|
15
|
+
# Arrays.
|
16
|
+
# @api private
|
8
17
|
class EnumerableEncoder < StreamingEncoder
|
9
18
|
include Enumerable
|
10
19
|
|
20
|
+
# Iterates over the body, encoding and yielding individual chunks
|
21
|
+
# of the response entity.
|
22
|
+
# @yield [chunk]
|
23
|
+
# @yieldparam [String] chunk a chunk of the response, encoded
|
11
24
|
def each
|
12
|
-
|
13
|
-
yield
|
25
|
+
body.each do |block|
|
26
|
+
yield resource.send(encoder, resource.send(charsetter, block.to_s))
|
14
27
|
end
|
15
28
|
end
|
16
29
|
end
|
17
30
|
|
31
|
+
# Implements a streaming encoder for callable bodies, such as
|
32
|
+
# Proc. (essentially futures)
|
33
|
+
# @api private
|
18
34
|
class CallableEncoder < StreamingEncoder
|
35
|
+
# Encodes the output of the body Proc.
|
36
|
+
# @return [String]
|
19
37
|
def call
|
20
|
-
|
38
|
+
resource.send(encoder, resource.send(charsetter, body.call.to_s))
|
21
39
|
end
|
22
40
|
|
41
|
+
# Converts this encoder into a Proc.
|
42
|
+
# @return [Proc] a closure that wraps the {#call} method
|
43
|
+
# @see #call
|
23
44
|
def to_proc
|
24
45
|
method(:call).to_proc
|
25
46
|
end
|
26
47
|
end
|
48
|
+
|
49
|
+
# Implements a streaming encoder for Fibers with the same API as the
|
50
|
+
# EnumerableEncoder. This will resume the Fiber until it terminates
|
51
|
+
# or returns a falsey value.
|
52
|
+
# @api private
|
53
|
+
class FiberEncoder < EnumerableEncoder
|
54
|
+
|
55
|
+
# Iterates over the body by yielding to the fiber.
|
56
|
+
# @api private
|
57
|
+
def each
|
58
|
+
while body.alive? && chunk = body.resume
|
59
|
+
yield resource.send(encoder, resource.send(charsetter, chunk.to_s))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
27
63
|
end
|
@@ -3,7 +3,14 @@ require 'i18n'
|
|
3
3
|
I18n.config.load_path << File.expand_path("../locale/en.yml", __FILE__)
|
4
4
|
|
5
5
|
module Webmachine
|
6
|
+
# Provides an interface to the I18n library specifically for
|
7
|
+
# {Webmachine}'s messages.
|
6
8
|
module Translation
|
9
|
+
# Interpolates an internationalized string.
|
10
|
+
# @param [String] key the name of the string to interpolate
|
11
|
+
# @param [Hash] options options to pass to I18n, including
|
12
|
+
# variables to interpolate.
|
13
|
+
# @return [String] the interpolated string
|
7
14
|
def t(key, options={})
|
8
15
|
::I18n.t(key, options.merge(:scope => :webmachine))
|
9
16
|
end
|