webmachine 0.2.0 → 0.3.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.
- 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
|