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.
@@ -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