webmachine 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,9 +12,12 @@ module Webmachine
12
12
  # order they are added.
13
13
  # @see Route#new
14
14
  def add_route(*args)
15
- @routes << Route.new(*args)
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
@@ -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
@@ -1,20 +1,30 @@
1
+ require 'cgi'
1
2
  require 'forwardable'
2
3
 
3
4
  module Webmachine
4
- # This represents a single HTTP request sent from a client.
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
- def initialize(meth, uri, headers, body)
11
- @method, @uri, @headers, @body = meth, uri, headers, body
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
- # @private
17
- def method_missing(m, *args)
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 = URI.unescape(kv).split(/=/)
59
+ k, v = CGI.unescape(kv).split(/=/)
50
60
  @query[k] = v if k && v
51
61
  end
52
62
  end
@@ -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 {#initialize} to
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 is a Symbol naming the method
199
- # which can provide a resource representation in that media
200
- # type. For example, if a client request includes an 'Accept'
201
- # header with a value that does not appear as a first element in
202
- # any of the return pairs, then a '406 Not Acceptable' will be
203
- # sent. Default is [['text/html', :to_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
@@ -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 {Callbacks#process_post}
37
- # to indicate that the client should request another resource
38
- # using GET. Either pass the URI of the target resource, or
39
- # manually set the Location header using {#headers}.
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
@@ -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 StreamingEncoder
3
- def initialize(resource, encoder, charsetter, body)
4
- @resource, @encoder, @charsetter, @body = resource, encoder, charsetter, body
5
- end
6
- end
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
- @body.each do |block|
13
- yield @resource.send(@encoder, @resource.send(@charsetter, block))
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
- @resource.send(@encoder, @resource.send(@charsetter, @body.call))
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
@@ -1,4 +1,8 @@
1
1
  module Webmachine
2
- VERSION = "0.2.0"
2
+ # Library version
3
+ VERSION = "0.3.0"
4
+
5
+ # String for use in "Server" HTTP response header, which includes
6
+ # the {VERSION}.
3
7
  SERVER_STRING = "Webmachine-Ruby/#{VERSION}"
4
8
  end