@@ -1,6 +1,8 @@
require 'webmachine/decision/helpers'
require 'webmachine/decision/fsm'
require 'webmachine/decision/helpers'
require 'webmachine/trace'
require 'webmachine/translation'
require 'webmachine/constants'
require 'webmachine/rescueable_exception'

module Webmachine
module Decision
@@ -9,23 +11,26 @@ module Decision
class FSM
include Flow
include Helpers
include Trace::FSM
include Translation

attr_reader :resource, :request, :response, :metadata

def initialize(resource, request, response)
@resource, @request, @response = resource, request, response
@metadata = {}
super
end

# Processes the request, iteratively invoking the decision methods in {Flow}.
def run
state = Flow::START
trace_request(request)
loop do
response.trace << state
result = send(state)
trace_decision(state)
result = handle_exceptions { send(state) }
case result
when Fixnum # Response code
when Integer # Response code
respond(result)
break
when Symbol # Next state
@@ -34,37 +39,43 @@ def run
raise InvalidResource, t('fsm_broke', :state => state, :result => result.inspect)
end
end
rescue MalformedRequest => malformed
Webmachine.render_error(400, request, response, :message => malformed.message)
respond(400)
rescue => e # Handle all exceptions without crashing the server
error_response(e, state)
rescue => e
Webmachine.render_error(500, request, response, :message => e.message)
ensure
trace_response(response)
end

private

def handle_exceptions
yield
rescue Webmachine::RescuableException => e
resource.handle_exception(e)
500
rescue MalformedRequest => e
Webmachine.render_error(400, request, response, :message => e.message)
400
end

def respond(code, headers={})
response.code = code
response.headers.merge!(headers)
end_time = Time.now
case code
when 404
Webmachine.render_error(code, request, response)
when 304
response.headers.delete('Content-Type')
response.headers.delete(CONTENT_TYPE)
add_caching_headers
end
response.code = code
resource.finish_request
# TODO: add logging/tracing
end

# Renders a 500 error by capturing the exception information.
def error_response(exception, state)
response.error = [exception.message, exception.backtrace].flatten.join("\n ")
response.end_state = state
Webmachine.render_error(500, request, response)
respond(500)
end
response.code = handle_exceptions do
resource.finish_request
response.code
end

ensure_content_length(response)
ensure_date_header(response)
end
end # class FSM
end # module Decision
end # module Webmachine
@@ -1,12 +1,19 @@
require 'stringio'
require 'time'
require 'webmachine/streaming'
require 'webmachine/media_type'
require 'webmachine/quoted_string'
require 'webmachine/etags'
require 'webmachine/header_negotiation'
require 'webmachine/constants'

module Webmachine
module Decision
# Methods that assist the Decision {Flow}.
module Helpers
# Pattern for quoted headers
QUOTED = /^"(.*)"$/
include QuotedString
include Streaming
include HeaderNegotiation

# Determines if the response has a body/entity set.
def has_response_body?
@@ -22,13 +29,15 @@ def encode_body_if_set
# Encodes the body in the selected charset and encoding.
def encode_body
body = response.body
chosen_charset = metadata['Charset']
chosen_encoding = metadata['Content-Encoding']
chosen_charset = metadata[CHARSET]
chosen_encoding = metadata[CONTENT_ENCODING]
charsetter = resource.charsets_provided && resource.charsets_provided.find {|c,_| c == chosen_charset }.last || :charset_nop
encoder = resource.encodings_provided[chosen_encoding]
response.body = case body
when String # 1.8 treats Strings as Enumerable
resource.send(encoder, resource.send(charsetter, body))
when IO, StringIO
IOEncoder.new(resource, encoder, charsetter, body)
when Fiber
FiberEncoder.new(resource, encoder, charsetter, body)
when Enumerable
@@ -40,29 +49,11 @@ def encode_body
resource.send(encoder, resource.send(charsetter, body))
end
end
if String === response.body
response.headers['Content-Length'] = response.body.respond_to?(:bytesize) ? response.body.bytesize.to_s : response.body.length.to_s
if body_is_fixed_length?
ensure_content_length(response)
else
response.headers.delete 'Content-Length'
response.headers['Transfer-Encoding'] = 'chunked'
end
end

# Ensures that a header is quoted (like ETag)
def ensure_quoted_header(value)
if value =~ QUOTED
value
else
'"' << value << '"'
end
end

# Unquotes request headers (like ETag)
def unquote_header(value)
if value =~ QUOTED
$1
else
value
response.headers.delete CONTENT_LENGTH
response.headers[TRANSFER_ENCODING] = 'chunked'
end
end

@@ -90,7 +81,7 @@ def variances
# Adds caching-related headers to the response.
def add_caching_headers
if etag = resource.generate_etag
response.headers['ETag'] = ensure_quoted_header(etag)
response.headers['ETag'] = ETag.new(etag).to_s
end
if expires = resource.expires
response.headers['Expires'] = expires.httpdate
@@ -99,6 +90,13 @@ def add_caching_headers
response.headers['Last-Modified'] = modified.httpdate
end
end

# Determines whether the response is of a fixed lenghth, i.e. it
# is a String or IO with known size.
def body_is_fixed_length?
response.body.respond_to?(:bytesize) &&
Integer === response.body.bytesize
end
end # module Helpers
end # module Decision
end # module Webmachine
@@ -1,19 +1,28 @@
require 'forwardable'
require 'forwardable'
require 'webmachine/decision'
require 'webmachine/dispatcher/route'

module Webmachine
# Handles dispatching incoming requests to the proper registered
# resources and initializing the decision logic.
class Dispatcher
WM_DISPATCH = 'wm.dispatch'.freeze

# @return [Array<Route>] the list of routes that will be
# dispatched to
# @see #add_route
attr_reader :routes

# The creator for resources used to process requests.
# Must respond to call(route, request, response) and return
# a newly created resource instance.
attr_accessor :resource_creator

# Initialize a Dispatcher instance
def initialize
# @param resource_creator Invoked to create resource instances.
def initialize(resource_creator = method(:create_resource))
@routes = []
@resource_creator = resource_creator
end

# Adds a route to the dispatch list. Routes will be matched in the
@@ -32,11 +41,14 @@ def add_route(*args, &block)
# @param [Request] request the request object
# @param [Response] response the response object
def dispatch(request, response)
route = @routes.find {|r| r.match?(request) }
if route
resource = route.resource.new(request, response)
route.apply(request)
Webmachine::Decision::FSM.new(resource, request, response).run
if resource = find_resource(request, response)
Webmachine::Events.instrument(WM_DISPATCH) do |payload|
Webmachine::Decision::FSM.new(resource, request, response).run

payload[:resource] = resource.class.name
payload[:request] = request.dup
payload[:code] = response.code
end
else
Webmachine.render_error(404, request, response)
end
@@ -47,6 +59,31 @@ def dispatch(request, response)
def reset
@routes.clear
end

# Find the first resource that matches an incoming request
# @param [Request] request the request to match
# @param [Response] response the response for the resource
def find_resource(request, response)
if route = find_route(request)
prepare_resource(route, request, response)
end
end

# Find the first route that matches an incoming request
# @param [Request] request the request to match
def find_route(request)
@routes.find {|r| r.match?(request) }
end

private
def prepare_resource(route, request, response)
route.apply(request)
@resource_creator.call(route, request, response)
end

def create_resource(route, request, response)
route.resource.new(request, response)
end
end

# Evaluates the passed block in the context of
@@ -1,11 +1,14 @@
require 'webmachine/resource'
require 'webmachine/resource'
require 'webmachine/translation'
require 'webmachine/constants'

module Webmachine
class Dispatcher
# Pairs URIs with {Resource} classes in the {Dispatcher}. To
# create routes, use {Dispatcher#add_route}.
class Route
include Translation

# @return [Class] the resource this route will dispatch to, a
# subclass of {Resource}
attr_reader :resource
@@ -20,13 +23,16 @@ class Route

# When used in a path specification, will match all remaining
# segments
MATCH_ALL = '*'.freeze
MATCH_ALL = :*

# String version of MATCH_ALL, deprecated. Use the symbol instead.
MATCH_ALL_STR = '*'.freeze

# Creates a new Route that will associate a pattern to a
# {Resource}.
#
# @example Standard route
# Route.new(["*"], MyResource)
# Route.new([:*], MyResource)
#
# @example Guarded route
# Route.new ["/notes"],
@@ -52,7 +58,7 @@ class Route
# @yield [req] an optional guard block
# @yieldparam [Request] req the request object
# @see Dispatcher#add_route
def initialize(path_spec, *args)
def initialize(path_spec, *args, &block)
if args.last.is_a? Hash
bindings = args.pop
else
@@ -61,7 +67,9 @@ def initialize(path_spec, *args)

resource = args.pop
guards = args
guards << Proc.new if block_given?
guards << block if block_given?

warn t('match_all_symbol') if path_spec.include? MATCH_ALL_STR

@path_spec = path_spec
@guards = guards
@@ -75,17 +83,17 @@ def initialize(path_spec, *args)
# should be dispatched to the {#resource}.
# @param [Reqeust] request the request object
def match?(request)
tokens = request.uri.path.match(/^\/(.*)/)[1].split('/')
tokens = request.routing_tokens
bind(tokens, {}) && guards.all? { |guard| guard.call(request) }
end

# Decorates the request with information about the dispatch
# route, including path bindings.
# @param [Request] request the request object
def apply(request)
request.disp_path = request.uri.path.match(/^\/(.*)/)[1]
request.disp_path = request.routing_tokens.join(SLASH)
request.path_info = @bindings.dup
tokens = request.disp_path.split('/')
tokens = request.routing_tokens
depth, trailing = bind(tokens, request.path_info)
request.path_tokens = trailing || []
end
@@ -95,7 +103,7 @@ def apply(request)
# accumulating variable bindings.
# @param [Array<String>] tokens the list of path segments
# @param [Hash] bindings where path bindings will be stored
# @return [Fixnum, Array<Fixnum, Array>, false] either the depth
# @return [Integer, Array<Integer, Array>, false] either the depth
# that the path matched at, the depth and tokens matched by
# {MATCH_ALL}, or false if it didn't match.
def bind(tokens, bindings)
@@ -105,12 +113,28 @@ def bind(tokens, bindings)
case
when spec.empty? && tokens.empty?
return depth
when spec == [MATCH_ALL_STR]
return [depth, tokens]
when spec == [MATCH_ALL]
return [depth, tokens]
when tokens.empty?
return false
when Regexp === spec.first
matches = spec.first.match Route.rfc3986_percent_decode(tokens.first)
if matches
if spec.first.named_captures.empty?
bindings[:captures] = (bindings[:captures] || []) + matches.captures
else
spec.first.named_captures.reduce(bindings) do |bindings, (name, idxs)|
bindings[name.to_sym] = matches.captures[idxs.first-1]
bindings
end
end
else
return false
end
when Symbol === spec.first
bindings[spec.first] = tokens.first
bindings[spec.first] = Route.rfc3986_percent_decode(tokens.first)
when spec.first == tokens.first
else
return false
@@ -121,6 +145,20 @@ def bind(tokens, bindings)
end
end

# Decode a string using the scheme described in RFC 3986 2.1. Percent-Encoding (https://www.ietf.org/rfc/rfc3986.txt)
def self.rfc3986_percent_decode(value)
s = StringScanner.new(value)
result = ''
until s.eos?
encoded_val = s.scan(/%([0-9a-fA-F]){2}/)
result << if encoded_val.nil?
s.getch
else
[encoded_val[1..-1]].pack('H*')
end
end
result
end
end # class Route
end # module Dispatcher
end # module Webmachine
@@ -1,12 +1,15 @@
require 'webmachine/header_negotiation'
require 'webmachine/translation'
require 'webmachine/constants'
require 'webmachine/version'

module Webmachine
extend HeaderNegotiation
extend Translation

# Renders a standard error message body for the response. The
# standard messages are defined in localization files.
# @param [Fixnum] code the response status code
# @param [Integer] code the response status code
# @param [Request] req the request object
# @param [Response] req the response object
# @param [Hash] options keys to override the defaults when rendering
@@ -21,10 +24,13 @@ def self.render_error(code, req, res, options={})
{:title => title,
:message => message,
:version => Webmachine::SERVER_STRING}.merge(options))
res.headers['Content-Type'] = "text/html"
res.headers[CONTENT_TYPE] = TEXT_HTML
end
ensure_content_length(res)
ensure_date_header(res)
end


# Superclass of all errors generated by Webmachine.
class Error < ::StandardError; end

@@ -33,6 +39,6 @@ class InvalidResource < Error; end

# Raised when the client has submitted an invalid request, e.g. in
# the case where a request header is improperly formed. Raising this
# exception will result in a 400 response.
# error will result in a 400 response.
class MalformedRequest < Error; end
end # module Webmachine
@@ -0,0 +1,69 @@
require 'webmachine/quoted_string'

module Webmachine
# A wrapper around entity tags that encapsulates their semantics.
# This class by itself represents a "strong" entity tag.
class ETag
include QuotedString
# The pattern for a weak entity tag
WEAK_ETAG = /^W\/#{QUOTED_STRING}$/.freeze

def self.new(etag)
return etag if ETag === etag
klass = etag =~ WEAK_ETAG ? WeakETag : self
klass.send(:allocate).tap do |obj|
obj.send(:initialize, etag)
end
end

attr_reader :etag

def initialize(etag)
@etag = quote(etag)
end

# An entity tag is equivalent to another entity tag if their
# quoted values are equivalent. It is also equivalent to a String
# which represents the equivalent ETag.
def ==(other)
case other
when ETag
other.etag == @etag
when String
quote(other) == @etag
end
end

# Converts the entity tag into a string appropriate for use in a
# header.
def to_s
quote @etag
end
end

# A Weak Entity Tag, which can be used to compare entities which are
# semantically equivalent, but do not have the same byte-content. A
# WeakETag is equivalent to another entity tag if the non-weak
# portions are equivalent. It is also equivalent to a String which
# represents the equivalent strong or weak ETag.
class WeakETag < ETag
# Converts the WeakETag to a String for use in a header.
def to_s
"W/#{super}"
end

private
def unquote(str)
if str =~ WEAK_ETAG
unescape_quotes $1
else
super
end
end

def quote(str)
str = unescape_quotes($1) if str =~ WEAK_ETAG
super
end
end
end
@@ -0,0 +1,179 @@
require 'securerandom' # For AS::Notifications
require 'as/notifications'
require 'webmachine/events/instrumented_event'

module Webmachine
# {Webmachine::Events} implements the
# [ActiveSupport::Notifications](http://rubydoc.info/gems/activesupport/ActiveSupport/Notifications)
# instrumentation API. It delegates to the configured backend.
# The default backend is
# [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications).
#
# # Published events
#
# Webmachine publishes some internal events by default. All of them use
# the `wm.` prefix.
#
# ## `wm.dispatch` ({.instrument})
#
# The payload hash includes the following keys.
#
# * `:resource` - The resource class name
# * `:request` - A copy of the request object
# * `:code` - The response code
#
module Events
class << self
# The class that {Webmachine::Events} delegates all messages to.
# (default [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications))
#
# It can be changed to an
# [ActiveSupport::Notifications](http://rubydoc.info/gems/activesupport/ActiveSupport/Notifications)
# compatible backend early in the application startup process.
#
# @example
# require 'webmachine'
# require 'active_support/notifications'
#
# Webmachine::Events.backend = ActiveSupport::Notifications
#
# Webmachine::Application.new {|app|
# # setup application
# }.run
attr_accessor :backend

# Publishes the given arguments to all listeners subscribed to the given
# event name.
# @param name [String] the event name
# @example
# Webmachine::Events.publish('wm.foo', :hello => 'world')
def publish(name, *args)
backend.publish(name, *args)
end

# Instrument the given block by measuring the time taken to execute it
# and publish it. Notice that events get sent even if an error occurs
# in the passed-in block.
#
# If an error happens during an instrumentation the payload will
# have a key `:exception` with an array of two elements as value:
# a string with the name of the error class, and the error
# message. (when using the default
# [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications)
# backend)
#
# @param name [String] the event name
# @param payload [Hash] the initial payload
#
# @example
# Webmachine::Events.instrument('wm.dispatch') do |payload|
# execute_some_method
#
# payload[:custom_payload_value] = 'important'
# end
def instrument(name, payload = {}, &block)
backend.instrument(name, payload, &block)
end

# Subscribes to the given event name.
#
# @note The documentation of this method describes its behaviour with the
# default backed [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications).
# It can change if a different backend is used.
#
# @overload subscribe(name)
# Subscribing to an {.instrument} event. The block arguments can be
# passed to {Webmachine::Events::InstrumentedEvent}.
#
# @param name [String, Regexp] the event name to subscribe to
# @yieldparam name [String] the event name
# @yieldparam start [Time] the event start time
# @yieldparam end [Time] the event end time
# @yieldparam event_id [String] the event id
# @yieldparam payload [Hash] the event payload
# @return [Object] the subscriber object (type depends on the backend implementation)
#
# @example
# # Subscribe to all 'wm.dispatch' events
# Webmachine::Events.subscribe('wm.dispatch') {|*args|
# event = Webmachine::Events::InstrumentedEvent.new(*args)
# }
#
# # Subscribe to all events that start with 'wm.'
# Webmachine::Events.subscribe(/wm\.*/) {|*args| }
#
# @overload subscribe(name)
# Subscribing to a {.publish} event.
#
# @param name [String, Regexp] the event name to subscribe to
# @yieldparam name [String] the event name
# @yieldparam *args [Array] the published arguments
# @return [Object] the subscriber object (type depends on the backend implementation)
#
# @example
# Webmachine::Events.subscribe('custom.event') {|name, *args|
# args #=> [obj1, obj2, {:num => 1}]
# }
#
# Webmachine::Events.publish('custom.event', obj1, obj2, {:num => 1})
#
# @overload subscribe(name, listener)
# Subscribing with a listener object instead of a block. The listener
# object must respond to `#call`.
#
# @param name [String, Regexp] the event name to subscribe to
# @param listener [#call] a listener object
# @return [Object] the subscriber object (type depends on the backend implementation)
#
# @example
# class CustomListener
# def call(name, *args)
# # ...
# end
# end
#
# Webmachine::Events.subscribe('wm.dispatch', CustomListener.new)
#
def subscribe(name, *args, &block)
backend.subscribe(name, *args, &block)
end

# Subscribe to an event temporarily while the block runs.
#
# @note The documentation of this method describes its behaviour with the
# default backed [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications).
# It can change if a different backend is used.
#
# The callback in the following example will be called for all
# "sql.active_record" events instrumented during the execution of the
# block. The callback is unsubscribed automatically after that.
#
# @example
# callback = lambda {|name, *args| handle_event(name, *args) }
#
# Webmachine::Events.subscribed(callback, 'sql.active_record') do
# call_active_record
# end
def subscribed(callback, name, &block)
backend.subscribed(callback, name, &block)
end

# Unsubscribes the given subscriber.
#
# @note The documentation of this method describes its behaviour with the
# default backed [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications).
# It can change if a different backend is used.
#
# @param subscriber [Object] the subscriber object (type depends on the backend implementation)
# @example
# subscriber = Webmachine::Events.subscribe('wm.dispatch') {|*args| }
#
# Webmachine::Events.unsubscribe(subscriber)
def unsubscribe(subscriber)
backend.unsubscribe(subscriber)
end
end

self.backend = AS::Notifications
end
end
@@ -0,0 +1,19 @@
require 'delegate'
require 'as/notifications/instrumenter'

module Webmachine
module Events
# {Webmachine::Events::InstrumentedEvent} delegates to
# [AS::Notifications::Event](http://rubydoc.info/gems/as-notifications/AS/Notifications/Event).
#
# The class
# [AS::Notifications::Event](http://rubydoc.info/gems/as-notifications/AS/Notifications/Event)
# is able to take the arguments of an {Webmachine::Events.instrument} event
# and provide an object-oriented interface to that data.
class InstrumentedEvent < SimpleDelegator
def initialize(*args)
super(AS::Notifications::Event.new(*args))
end
end
end
end

This file was deleted.

@@ -0,0 +1,25 @@
require 'webmachine/constants'

module Webmachine
module HeaderNegotiation
def ensure_date_header(res)
if (200..499).include?(res.code)
res.headers[DATE] ||= Time.now.httpdate
end
end

def ensure_content_length(res)
body = res.body
case
when res.headers[TRANSFER_ENCODING]
return
when [204, 205, 304].include?(res.code)
res.headers.delete CONTENT_LENGTH
when body != nil
res.headers[CONTENT_LENGTH] = body.respond_to?(:bytesize) ? body.bytesize.to_s : body.length.to_s
else
res.headers[CONTENT_LENGTH] = '0'
end
end
end
end
@@ -1,17 +1,41 @@
require 'webmachine/constants'

module Webmachine
# Case-insensitive Hash of Request headers
class Headers < ::Hash
CGI_HTTP_MATCH = /^HTTP_(\w+)$/.freeze
CONTENT_TYPE_LENGTH_MATCH = /^(CONTENT_(?:TYPE|LENGTH))$/.freeze

# Convert CGI-style Hash into Request headers
# @param [Hash] env a hash of CGI-style env/headers
# @return [Webmachine::Headers]
def self.from_cgi(env)
env.inject(new) do |h,(k,v)|
if k =~ /^HTTP_(\w+)$/ || k =~ /^(CONTENT_(?:TYPE|LENGTH))$/
h[$1.tr("_", "-")] = v
if k =~ CGI_HTTP_MATCH || k =~ CONTENT_TYPE_LENGTH_MATCH
h[$1.tr(UNDERSCORE, DASH)] = v
end
h
end
end

# Creates a new headers object populated with the given objects.
# It supports the same forms as {Hash.[]}.
#
# @overload [](key, value, ...)
# Pairs of keys and values
# @param [Object] key
# @param [Object] value
# @overload [](array)
# Array of key-value pairs
# @param [Array<Object, Object>, ...]
# @overload [](object)
# Object convertible to a hash
# @param [Object]
# @return [Webmachine::Headers]
def self.[](*args)
super(super(*args).map {|k, v| [k.to_s.downcase, v]})
end

# Fetch a header
def [](key)
super transform_key(key)
@@ -22,6 +46,29 @@ def []=(key,value)
super transform_key(key), value
end

# Returns the value for the given key. If the key can't be found,
# there are several options:
# With no other arguments, it will raise a KeyError error;
# if default is given, then that will be returned;
# if the optional code block is specified, then that will be run and its
# result returned.
#
# @overload fetch(key)
# A key
# @param [Object] key
# @overload fetch(key, default)
# A key and a default value
# @param [Object] key
# @param [Object] default
# @overload fetch(key) {|key| block }
# A key and a code block
# @param [Object]
# @yield [key] Passes the key to the block
# @return [Object] the value for the key or the default
def fetch(*args, &block)
super(transform_key(args.shift), *args, &block)
end

# Delete a header
def delete(key)
super transform_key(key)
@@ -1,10 +1,11 @@
en:
webmachine:
errors:
standard_body: "<!DOCTYPE html><html>
<head><title>%{title}</title></head>
<body><h1>%{title}</h1><p>%{message}</p>
<address>%{version} server</address></body></html>"
standard_body: "<!DOCTYPE html><html>\n
<head><title>%{title}</title></head>\n
<body><h1>%{title}</h1>\n
<p>%{message}</p>\n
<address>%{version} server</address></body></html>\n"
"400":
title: 400 Malformed Request
message: The request was malformed and could not be processed.
@@ -13,7 +14,7 @@ en:
message: The requested document was not found on this server.
"500":
title: 500 Internal Server Error
message: "The server encountered an error while processing this request: <pre>%{error}</pre>"
message: "The server encountered an error while processing this request: <pre>\n%{error}</pre>"
"501":
title: 501 Not Implemented
message: "The server does not support the %{method} method."
@@ -26,3 +27,4 @@ en:
invalid_media_type: "Invalid media type: %{type}"
not_resource_class: "%{class} is not a subclass of Webmachine::Resource"
process_post_invalid: "process_post returned %{result}"
match_all_symbol: '"*" as a path segment is deprecated and will be removed in a future release. Please use :*'
@@ -1,14 +1,16 @@
require 'webmachine/translation'
require 'webmachine/translation'
require 'webmachine/constants'
require 'webmachine/dispatcher/route'

module Webmachine
# Encapsulates a MIME media type, with logic for matching types.
class MediaType
extend Translation
# Matches valid media types
MEDIA_TYPE_REGEX = /^\s*([^;\s]+)\s*((?:;\s*\S+\s*)*)\s*$/
MEDIA_TYPE_REGEX = /^\s*([^;\s]+)\s*((?:;\s*\S+\s*)*)\s*$/.freeze

# Matches sub-type parameters
PARAMS_REGEX = /;\s*([^=]+)=([^;=\s]+)/
PARAMS_REGEX = /;\s*([^=]+)(=([^;=\s]*))?/.freeze

# Creates a new MediaType by parsing an alternate representation.
# @param [MediaType, String, Array<String,Hash>] obj the raw type
@@ -21,7 +23,7 @@ def self.parse(obj)
obj
when MEDIA_TYPE_REGEX
type, raw_params = $1, $2
params = Hash[raw_params.scan(PARAMS_REGEX)]
params = Hash[raw_params.scan(PARAMS_REGEX).map { |m| [m[0], m[2].to_s] }]
new(type, params)
else
unless Array === obj && String === obj[0] && Hash === obj[1]
@@ -48,7 +50,7 @@ def initialize(type, params={})
# Detects whether the {MediaType} represents an open wildcard
# type, that is, "*/*" without any {#params}.
def matches_all?
@type == "*/*" && @params.empty?
@type == MATCHES_ALL && @params.empty?
end

# @return [true,false] Are these two types strictly equal?
@@ -62,7 +64,7 @@ def ==(other)
# Detects whether this {MediaType} matches the other {MediaType},
# taking into account wildcards. Sub-type parameters are treated
# strictly.
# @param [MediaType, String, Array<String,Hash>] other the other type
# @param [MediaType, String, Array<String,Hash>] other the other type
# @return [true,false] whether it is an acceptable match
def exact_match?(other)
other = self.class.parse(other)
@@ -73,7 +75,7 @@ def exact_match?(other)
# other {MediaType}, taking into account wildcards and satisfying
# all requested parameters, but allowing this type to have extra
# specificity.
# @param [MediaType, String, Array<String,Hash>] other the other type
# @param [MediaType, String, Array<String,Hash>] other the other type
# @return [true,false] whether it is an acceptable match
def match?(other)
other = self.class.parse(other)
@@ -88,7 +90,7 @@ def match?(other)
def params_match?(other)
other.all? {|k,v| params[k] == v }
end

# Reconstitutes the type into a String
# @return [String] the type as a String
def to_s
@@ -97,23 +99,23 @@ def to_s

# @return [String] The major type, e.g. "application", "text", "image"
def major
type.split("/").first
type.split(SLASH).first
end

# @return [String] the minor or sub-type, e.g. "json", "html", "jpeg"
def minor
type.split("/").last
type.split(SLASH).last
end

# @param [MediaType] other the other type
# @return [true,false] whether the main media type is acceptable,
# ignoring params and taking into account wildcards
def type_matches?(other)
other = self.class.parse(other)
if ["*", "*/*", type].include?(other.type)
if [Dispatcher::Route::MATCH_ALL_STR, MATCHES_ALL, type].include?(other.type)
true
else
other.major == major && other.minor == "*"
other.major == major && other.minor == Dispatcher::Route::MATCH_ALL_STR
end
end
end # class MediaType
@@ -0,0 +1,39 @@
module Webmachine
# Helper methods for dealing with the 'quoted-string' type often
# present in header values.
module QuotedString
# The pattern for a 'quoted-string' type
QUOTED_STRING = /"((?:\\"|[^"])*)"/.freeze

# The pattern for a 'quoted-string' type, without any other content.
QS_ANCHORED = /^#{QUOTED_STRING}$/.freeze

# Removes surrounding quotes from a quoted-string
def unquote(str)
if str =~ QS_ANCHORED
unescape_quotes $1
else
str
end
end

# Ensures that quotes exist around a quoted-string
def quote(str)
if str =~ QS_ANCHORED
str
else
%Q{"#{escape_quotes str}"}
end
end

# Escapes quotes within a quoted string.
def escape_quotes(str)
str.gsub(/"/, '\\"')
end

# Unescapes quotes within a quoted string
def unescape_quotes(str)
str.gsub(%r{\\}, '')
end
end
end
@@ -1,24 +1,34 @@
require 'cgi'
require 'cgi'
require 'forwardable'
require 'webmachine/constants'
require 'ipaddr'

module Webmachine
# Request represents a single HTTP request sent from a client. It
# should be instantiated by {Adapters} when a request is received
class Request
HTTP_HEADERS_MATCH = /^(?:[a-z0-9])+(?:_[a-z0-9]+)*$/i.freeze
ROUTING_PATH_MATCH = /^\/(.*)/.freeze

extend Forwardable
attr_reader :method, :uri, :headers, :body
attr_accessor :disp_path, :path_info, :path_tokens

STANDARD_HTTP_METHODS = %w[GET HEAD POST PUT DELETE TRACE CONNECT OPTIONS]
attr_reader :method, :uri, :headers, :body, :routing_tokens, :base_uri
attr_accessor :disp_path, :path_info, :path_tokens

# @param [String] method the HTTP request method
# @param [URI] uri the requested URI, including host, scheme and
# port
# @param [Headers] headers the HTTP request headers
# @param [String,#to_s,#each,nil] body the entity included in the
# request, if present
def initialize(method, uri, headers, body)
@method, @uri, @headers, @body = method, uri, headers, body
def initialize(method, uri, headers, body, routing_tokens=nil, base_uri=nil)
@method, @headers, @body = method, headers, body
@uri = build_uri(uri, headers)
@routing_tokens = routing_tokens || @uri.path.match(ROUTING_PATH_MATCH)[1].split(SLASH)
@base_uri = base_uri || @uri.dup.tap do |u|
u.path = SLASH
u.query = nil
end
end

def_delegators :headers, :[]
@@ -27,9 +37,18 @@ def initialize(method, uri, headers, body)
# lowercased-underscored version of the header name, e.g.
# `if_unmodified_since`.
def method_missing(m, *args, &block)
if m.to_s =~ /^(?:[a-z0-9])+(?:_[a-z0-9]+)*$/i
if m =~ HTTP_HEADERS_MATCH
# Access headers more easily as underscored methods.
self[m.to_s.tr('_', '-')]
header_name = m.to_s.tr(UNDERSCORE, DASH)
if (header_value = @headers[header_name])
# Make future lookups faster.
self.class.class_eval <<-RUBY, __FILE__, __LINE__
def #{m}
@headers["#{header_name}"]
end
RUBY
end
header_value
else
super
end
@@ -39,16 +58,6 @@ def method_missing(m, *args, &block)
def has_body?
!(body.nil? || body.empty?)
end

# The root URI for the request, ignoring path and query. This is
# useful for calculating relative paths to resources.
# @return [URI]
def base_uri
@base_uri ||= uri.dup.tap do |u|
u.path = "/"
u.query = nil
end
end

# Returns a hash of query parameters (they come after the ? in the
# URI). Note that this does NOT work in the same way as Rails,
@@ -58,13 +67,27 @@ def query
unless @query
@query = {}
(uri.query || '').split(/&/).each do |kv|
k, v = CGI.unescape(kv).split(/=/)
@query[k] = v if k && v
key, value = kv.split(/=/)
if key && value
key, value = CGI.unescape(key), CGI.unescape(value)
@query[key] = value
end
end
end
@query
end

# The cookies sent in the request.
#
# @return [Hash]
# {} if no Cookies header set
def cookies
unless @cookies
@cookies = Webmachine::Cookie.parse(headers['Cookie'])
end
@cookies
end

# Is this an HTTPS request?
#
# @return [Boolean]
@@ -78,63 +101,95 @@ def https?
# @return [Boolean]
# true if this request was made with the GET method
def get?
method == "GET"
method == GET_METHOD
end

# Is this a HEAD request?
#
# @return [Boolean]
# true if this request was made with the HEAD method
def head?
method == "HEAD"
method == HEAD_METHOD
end

# Is this a POST request?
#
# @return [Boolean]
# true if this request was made with the GET method
def post?
method == "POST"
method == POST_METHOD
end

# Is this a PUT request?
#
# @return [Boolean]
# true if this request was made with the PUT method
def put?
method == "PUT"
method == PUT_METHOD
end

# Is this a DELETE request?
#
# @return [Boolean]
# true if this request was made with the DELETE method
def delete?
method == "DELETE"
method == DELETE_METHOD
end

# Is this a TRACE request?
#
# @return [Boolean]
# true if this request was made with the TRACE method
def trace?
method == "TRACE"
method == TRACE_METHOD
end

# Is this a CONNECT request?
#
# @return [Boolean]
# true if this request was made with the CONNECT method
def connect?
method == "CONNECT"
method == CONNECT_METHOD
end

# Is this an OPTIONS request?
#
# @return [Boolean]
# true if this request was made with the OPTIONS method
def options?
method == "OPTIONS"
method == OPTIONS_METHOD
end

private

IPV6_MATCH = /\A\[(?<address> .* )\]:(?<port> \d+ )\z/x.freeze # string like "[::1]:80"
HOST_MATCH = /\A(?<host> [^:]+ ):(?<port> \d+ )\z/x.freeze # string like "www.example.com:80"

def parse_host(uri, host_string)
# Split host and port number from string.
case host_string
when IPV6_MATCH
uri.host = IPAddr.new($~[:address], Socket::AF_INET6).to_s
uri.port = $~[:port].to_i
when HOST_MATCH
uri.host = $~[:host]
uri.port = $~[:port].to_i
else # string with no port number
uri.host = host_string
end

uri
end

def build_uri(uri, headers)
uri = URI(uri)
uri.port ||= 80
uri.scheme ||= HTTP
if uri.host
return uri
end

parse_host(uri, headers.fetch(HOST))
end

end # class Request
@@ -0,0 +1,62 @@
module Webmachine::RescuableException
require_relative 'errors'
require 'set'

UNRESCUABLE_DEFAULTS = [
Webmachine::MalformedRequest,
NoMemoryError, SystemExit, SignalException
].freeze

UNRESCUABLE = Set.new UNRESCUABLE_DEFAULTS.dup
private_constant :UNRESCUABLE

def self.===(e)
case e
when *UNRESCUABLE then false
else true
end
end

#
# Remove modifications to Webmachine::RescuableException.
# Restores default list of unrescue-able exceptions.
#
# @return [nil]
#
def self.default!
UNRESCUABLE.replace Set.new(UNRESCUABLE_DEFAULTS.dup)
nil
end

#
# @return [Array<Exception>]
# Returns an Array of exceptions that will not be
# rescued by {Webmachine::Resource#handle_exception}.
#
def self.UNRESCUABLEs
UNRESCUABLE.to_a
end

#
# Add a variable number of exceptions that should be rescued by
# {Webmachine::Resource#handle_exception}. See {UNRESCUABLE_DEFAULTS}
# for a list of exceptions that are not caught by default.
#
# @param (see #remove)
#
def self.add(*exceptions)
exceptions.each{|e| UNRESCUABLE.delete(e)}
end

#
# Remove a variable number of exceptions from being rescued by
# {Webmachine::Resource#handle_exception}. See {UNRESCUABLE_DEFAULTS}
# for a list of exceptions that are not caught by default.
#
# @param [Exception] *exceptions
# A subclass of Exception.
#
def self.remove(*exceptions)
exceptions.each{|e| UNRESCUABLE.add(e)}
end
end
@@ -1,6 +1,8 @@
require 'webmachine/resource/callbacks'
require 'webmachine/resource/encodings'
require 'webmachine/resource/entity_tags'
require 'webmachine/resource/authentication'
require 'webmachine/resource/tracing'

module Webmachine
# Resource is the primary building block of Webmachine applications,
@@ -21,6 +23,8 @@ module Webmachine
class Resource
include Callbacks
include Encodings
include EntityTags
include Tracing

attr_reader :request, :response

@@ -38,7 +42,22 @@ def self.new(request, response)
instance.send :initialize
instance
end


#
# Starts a web server that serves requests for a subclass of
# Webmachine::Resource.
#
# @return [void]
#
def self.run
resource = self
Application.new do |app|
app.routes do |router|
router.add [:*], resource
end
end.run
end

private
# When no specific charsets are provided, this acts as an identity
# on the response body. Probably deserves some refactoring.
@@ -11,7 +11,7 @@ module Authentication
# A simple implementation of HTTP Basic auth. Call this from the
# {Webmachine::Resource::Callbacks#is_authorized?} callback,
# giving it a block which will be yielded the username and
# password and return true or false.
# password and return true or false.
# @param [String] header the value of the Authentication request
# header, passed to the {Callbacks#is_authorized?} callback.
# @param [String] realm the "realm", or description of the
@@ -1,3 +1,5 @@
require 'webmachine/constants'

module Webmachine
class Resource
# These methods are the primary way your {Webmachine::Resource}
@@ -88,7 +90,7 @@ def known_content_type?(content_type = nil)

# If the request includes any invalid Content-* headers, this
# should return false, which will result in a '501 Not
# Implemented' response. Defaults to false.
# Implemented' response. Defaults to true.
# @param [Hash] content_headers Request headers that begin with
# 'Content-'
# @return [true,false] Whether the Content-* headers are invalid
@@ -101,7 +103,7 @@ def valid_content_headers?(content_headers = nil)
# If the entity length on PUT or POST is invalid, this should
# return false, which will result in a '413 Request Entity Too
# Large' response. Defaults to true.
# @param [Fixnum] length the size of the request body (entity)
# @param [Integer] length the size of the request body (entity)
# @return [true,false] Whether the body is a valid length (not too
# large)
# @api callback
@@ -123,7 +125,7 @@ def options
# @return [Array<String>] allowed methods on this resource
# @api callback
def allowed_methods
['GET', 'HEAD']
[GET_METHOD, HEAD_METHOD]
end

# HTTP methods that are known to the resource. Like
@@ -134,7 +136,7 @@ def allowed_methods
# @return [Array<String>] known methods
# @api callback
def known_methods
['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS']
STANDARD_HTTP_METHODS
end

# This method is called when a DELETE request should be enacted,
@@ -190,7 +192,7 @@ def base_uri

# If post_is_create? returns false, then this will be called to
# process any POST request. If it succeeds, it should return true.
# @return [true,false,Fixnum] Whether the POST was successfully
# @return [true,false,Integer] Whether the POST was successfully
# processed, or an alternate response code
# @api callback
def process_post
@@ -209,7 +211,7 @@ def process_post
# @return an array of mediatype/handler pairs
# @api callback
def content_types_provided
[['text/html', :to_html]]
[[TEXT_HTML, :to_html]]
end

# Similarly to content_types_provided, this should return an array
@@ -263,7 +265,7 @@ def language_chosen(lang)
# @api callback
# @see Encodings
def encodings_provided
{"identity" => :encode_identity }
{IDENTITY => :encode_identity }
end

# If this method is implemented, it should return a list of
@@ -360,6 +362,22 @@ def generate_etag
# @api callback
def finish_request; end

#
# This method is called when an error is raised within a subclass of
# {Webmachine::Resource}.
#
# @param [StandardError] e
# The error.
#
# @return [void]
#
# @api callback
#
def handle_exception(e)
response.error = [e.message, e.backtrace].flatten.join("\n ")
Webmachine.render_error(500, request, response)
end

# This method is called when verifying the Content-MD5 header
# against the request body. To do your own validation, implement
# it in this callback, returning true or false. To bypass header
@@ -0,0 +1,17 @@
require 'webmachine/etags'

module Webmachine
class Resource
module EntityTags
# Marks a generated entity tag (etag) as "weak", meaning that
# other representations of the resource may be semantically equivalent.
# @return [WeakETag] a weak version of the given ETag string
# @param [String] str the ETag to mark as weak
# @see http://tools.ietf.org/html/rfc2616#section-13.3.3
# @see http://tools.ietf.org/html/rfc2616#section-14.19
def weak_etag(str)
WeakETag.new(str)
end
end
end
end
@@ -0,0 +1,20 @@
module Webmachine
class Resource
# Contains {Resource} methods related to the visual debugger.
module Tracing
# Whether this resource should be traced. By default, tracing is
# disabled, but you can override it by setting the @trace
# instance variable in the initialize method, or by overriding
# this method. When enabled, traces can be visualized using the
# web debugging interface.
# @example
# def initialize
# @trace = true
# end
# @api callback
def trace?
!!@trace
end
end
end
end
@@ -1,10 +1,10 @@
module Webmachine
# Represents an HTTP response from Webmachine.
class Response
# @return [Hash] Response headers that will be sent to the client
# @return [HeaderHash] Response headers that will be sent to the client
attr_reader :headers

# @return [Fixnum] The HTTP status code of the response
# @return [Integer] The HTTP status code of the response
attr_accessor :code

# @return [String, #each] The response body
@@ -15,21 +15,17 @@ class Response

# @return [Array] the list of states that were traversed
attr_reader :trace

# @return [Symbol] When an error has occurred, the last state the
# FSM was in
attr_accessor :end_state

# @return [String] The error message when responding with an error
# code
attr_accessor :error

# Creates a new Response object with the appropriate defaults.
def initialize
@headers = {}
@headers = HeaderHash.new
@trace = []
self.code = 200
self.redirect = false
self.redirect = false
end

# Indicate that the response should be a redirect. This is only
@@ -44,8 +40,40 @@ def do_redirect(location=nil)
self.redirect = true
end

# Set a cookie for the response.
# @param [String, Symbol] name the name of the cookie
# @param [String] value the value of the cookie
# @param [Hash] attributes for the cookie. See RFC2109.
def set_cookie(name, value, attributes = {})
cookie = Webmachine::Cookie.new(name, value, attributes).to_s
case headers['Set-Cookie']
when nil
headers['Set-Cookie'] = [cookie]
when Array
headers['Set-Cookie'] << cookie
end
end

alias :is_redirect? :redirect
alias :redirect_to :do_redirect

# A {Hash} that can flatten array values into single values with a separator
class HeaderHash < ::Hash
# Return a new array with any {Array} values combined with the separator
# @param [String] The separator used to join Array values
# @return [HeaderHash] A new {HeaderHash} with Array values flattened
def flattened(separator = ',')
Hash[self.collect { |k,v|
case v
when Array
[k,v.join(separator)]
else
[k,v]
end
}]

end
end

end # class Response
end # module Webmachine
@@ -0,0 +1 @@
IO response body
@@ -0,0 +1,171 @@
require "webmachine/spec/test_resource"
require "net/http"

ADDRESS = "127.0.0.1"

shared_examples_for :adapter_lint do
attr_reader :client

class TestApplicationNotResponsive < Timeout::Error; end

def find_free_port
temp_server = TCPServer.new(ADDRESS, 0)
port = temp_server.addr[1]
temp_server.close # only frees Ruby resource, socket is in TIME_WAIT at OS level
# so we can't have our adapter use it too quickly

sleep(0.1) # 'Wait' for temp_server to *really* close, not just TIME_WAIT
port
end

def create_test_application(port)
Webmachine::Application.new.tap do |application|
application.dispatcher.add_route ["test"], Test::Resource

application.configure do |c|
c.ip = ADDRESS
c.port = port
end
end
end

def run_application(adapter_class, application)
adapter = adapter_class.new(application)
Thread.abort_on_exception = true
Thread.new { adapter.run }
end

def wait_until_server_responds_to(client)
Timeout.timeout(5, TestApplicationNotResponsive) do
begin
client.start
rescue Errno::ECONNREFUSED
sleep(0.01)
retry
end
end
end

before(:all) do
@port = find_free_port
application = create_test_application(@port)

adapter_class = described_class
@server_thread = run_application(adapter_class, application)

@client = Net::HTTP.new(application.configuration.ip, @port)
wait_until_server_responds_to(client)
end

after(:all) do
@client.finish
@server_thread.kill
end

it "provides the request URI" do
request = Net::HTTP::Get.new("/test")
request["Accept"] = "test/response.request_uri"
response = client.request(request)
expect(response.body).to eq("http://#{ADDRESS}:#{@port}/test")
end

# context do
# let(:address) { "::1" }

# it "provides the IPv6 request URI" do
# request = Net::HTTP::Get.new("/test")
# request["Accept"] = "test/response.request_uri"
# response = client.request(request)
# expect(response.body).to eq("http://[#{address}]:#{port}/test")
# end
# end

it "provides a string-like request body" do
request = Net::HTTP::Put.new("/test")
request.body = "Hello, World!"
request["Content-Type"] = "test/request.stringbody"
response = client.request(request)
expect(response["Content-Length"]).to eq("21")
expect(response.body).to eq("String: Hello, World!")
end

it "provides an enumerable request body" do
request = Net::HTTP::Put.new("/test")
request.body = "Hello, World!"
request["Content-Type"] = "test/request.enumbody"
response = client.request(request)
expect(response["Content-Length"]).to eq("19")
expect(response.body).to eq("Enum: Hello, World!")
end

it "handles missing pages" do
request = Net::HTTP::Get.new("/missing")
response = client.request(request)
expect(response.code).to eq("404")
expect(response["Content-Type"]).to eq("text/html")
end

it "handles empty response bodies" do
request = Net::HTTP::Post.new("/test")
request.body = ""
response = client.request(request)
expect(response.code).to eq("204")
expect(["0", nil]).to include(response["Content-Length"])
expect(response.body).to be_nil
end

it "handles string response bodies" do
request = Net::HTTP::Get.new("/test")
request["Accept"] = "test/response.stringbody"
response = client.request(request)
expect(response["Content-Length"]).to eq("20")
expect(response.body).to eq("String response body")
end

it "handles enumerable response bodies" do
request = Net::HTTP::Get.new("/test")
request["Accept"] = "test/response.enumbody"
response = client.request(request)
expect(response["Transfer-Encoding"]).to eq("chunked")
expect(response.body).to eq("Enumerable response body")
end

it "handles proc response bodies" do
request = Net::HTTP::Get.new("/test")
request["Accept"] = "test/response.procbody"
response = client.request(request)
expect(response["Transfer-Encoding"]).to eq("chunked")
expect(response.body).to eq("Proc response body")
end

it "handles fiber response bodies" do
request = Net::HTTP::Get.new("/test")
request["Accept"] = "test/response.fiberbody"
response = client.request(request)
expect(response["Transfer-Encoding"]).to eq("chunked")
expect(response.body).to eq("Fiber response body")
end

it "handles io response bodies" do
request = Net::HTTP::Get.new("/test")
request["Accept"] = "test/response.iobody"
response = client.request(request)
expect(response["Content-Length"]).to eq("17")
expect(response.body).to eq("IO response body\n")
end

it "handles request cookies" do
request = Net::HTTP::Get.new("/test")
request["Accept"] = "test/response.cookies"
request["Cookie"] = "echo=echocookie"
response = client.request(request)
expect(response.body).to eq("echocookie")
end

it "handles response cookies" do
request = Net::HTTP::Get.new("/test")
request["Accept"] = "test/response.cookies"
response = client.request(request)
expect(response["Set-Cookie"]).to eq("cookie=monster, rodeo=clown")
end
end
@@ -0,0 +1,84 @@
module Test
class Resource < Webmachine::Resource
def allowed_methods
["GET", "PUT", "POST"]
end

def content_types_accepted
[
["test/request.stringbody", :from_string],
["test/request.enumbody", :from_enum]
]
end

def content_types_provided
[
["test/response.stringbody", :to_string],
["test/response.enumbody", :to_enum],
["test/response.procbody", :to_proc],
["test/response.fiberbody", :to_fiber],
["test/response.iobody", :to_io_body],
["test/response.cookies", :to_cookies],
["test/response.request_uri", :to_request_uri],
["test/response.rack_env", :to_rack_env]
]
end

def from_string
response.body = "String: #{request.body.to_s}"
end

def from_enum
response.body = "Enum: "
request.body.each do |part|
response.body += part
end
end

# Response intentionally left blank to test 204 support
def process_post
true
end

def to_string
"String response body"
end

def to_enum
["Enumerable ", "response " "body"]
end

def to_proc
Proc.new { "Proc response body" }
end

def to_fiber
Fiber.new do
Fiber.yield "Fiber "
Fiber.yield "response "
"body"
end
end

def to_io_body
File.new(File.expand_path('../IO_response.body', __FILE__))
end

def to_cookies
response.set_cookie("cookie", "monster")
response.set_cookie("rodeo", "clown")
# FIXME: Mongrel/WEBrick fail if this method returns nil
# Might be a net/http issue. Is this a bug?
# @see Flow#o18, Helpers#encode_body_if_set
request.cookies["echo"] || ""
end

def to_request_uri
request.uri.to_s
end

def to_rack_env
request.env.to_json
end
end
end
@@ -1,63 +1,11 @@
begin
require 'fiber'
rescue LoadError
require 'webmachine/fiber18'
end

module Webmachine
# Subclasses of this class implement means for streamed/chunked
# response bodies to be coerced to the negotiated character set and
# encoded automatically as they are output to the client.
# @api private
StreamingEncoder = Struct.new(:resource, :encoder, :charsetter, :body)

# Implements a streaming encoder for Enumerable response bodies, such as
# Arrays.
# @api private
class EnumerableEncoder < StreamingEncoder
include Enumerable

# Iterates over the body, encoding and yielding individual chunks
# of the response entity.
# @yield [chunk]
# @yieldparam [String] chunk a chunk of the response, encoded
def each
body.each do |block|
yield resource.send(encoder, resource.send(charsetter, block.to_s))
end
end
end # class EnumerableEncoder

# Implements a streaming encoder for callable bodies, such as
# Proc. (essentially futures)
# @api private
class CallableEncoder < StreamingEncoder
# Encodes the output of the body Proc.
# @return [String]
def call
resource.send(encoder, resource.send(charsetter, body.call.to_s))
end

# Converts this encoder into a Proc.
# @return [Proc] a closure that wraps the {#call} method
# @see #call
def to_proc
method(:call).to_proc
end
end # class CallableEncoder

# Implements a streaming encoder for Fibers with the same API as the
# EnumerableEncoder. This will resume the Fiber until it terminates
# or returns a falsey value.
# @api private
class FiberEncoder < EnumerableEncoder

# Iterates over the body by yielding to the fiber.
# @api private
def each
while body.alive? && chunk = body.resume
yield resource.send(encoder, resource.send(charsetter, chunk.to_s))
end
end
end # class FiberEncoder
# Namespace for classes that support streaming response bodies.
module Streaming
end # module Streaming
end # module Webmachine

require 'webmachine/streaming/encoder'
require 'webmachine/streaming/enumerable_encoder'
require 'webmachine/streaming/io_encoder'
require 'webmachine/streaming/callable_encoder'
require 'webmachine/streaming/fiber_encoder'
@@ -0,0 +1,21 @@
module Webmachine
module Streaming
# Implements a streaming encoder for callable bodies, such as
# Proc. (essentially futures)
# @api private
class CallableEncoder < Encoder
# Encodes the output of the body Proc.
# @return [String]
def call
resource.send(encoder, resource.send(charsetter, body.call.to_s))
end

# Converts this encoder into a Proc.
# @return [Proc] a closure that wraps the {#call} method
# @see #call
def to_proc
method(:call).to_proc
end
end # class CallableEncoder
end
end
@@ -0,0 +1,24 @@
module Webmachine
module Streaming
# Subclasses of this class implement means for streamed/chunked
# response bodies to be coerced to the negotiated character set and
# encoded automatically as they are output to the client.
# @api private
class Encoder
attr_accessor :resource, :encoder, :charsetter, :body

def initialize(resource, encoder, charsetter, body)
@resource, @encoder, @charsetter, @body = resource, encoder, charsetter, body
end

protected
# @return [true, false] whether the stream will be modified by
# the encoder and/or charsetter. Only returns true if using the
# built-in "encode_identity" and "charset_nop" methods.
def is_unencoded?
encoder.to_s == "encode_identity" &&
charsetter.to_s == "charset_nop"
end
end # class Encoder
end # module Streaming
end # module Webmachine
@@ -0,0 +1,20 @@
module Webmachine
module Streaming
# Implements a streaming encoder for Enumerable response bodies, such as
# Arrays.
# @api private
class EnumerableEncoder < Encoder
include Enumerable

# Iterates over the body, encoding and yielding individual chunks
# of the response entity.
# @yield [chunk]
# @yieldparam [String] chunk a chunk of the response, encoded
def each
body.each do |block|
yield resource.send(encoder, resource.send(charsetter, block.to_s))
end
end
end # class EnumerableEncoder
end # module Streaming
end # module Webmachine
@@ -0,0 +1,21 @@
require 'fiber'

module Webmachine
module Streaming
# Implements a streaming encoder for Fibers with the same API as the
# EnumerableEncoder. This will resume the Fiber until it terminates
# or returns a falsey value.
# @api private
class FiberEncoder < Encoder
include Enumerable

# Iterates over the body by yielding to the fiber.
# @api private
def each
while body.alive? && chunk = body.resume
yield resource.send(encoder, resource.send(charsetter, chunk.to_s))
end
end
end # class FiberEncoder
end
end
@@ -0,0 +1,75 @@
require 'stringio'

module Webmachine
module Streaming
# Implements a streaming encoder for IO response bodies, such as
# File objects.
# @api private
class IOEncoder < Encoder
include Enumerable
CHUNK_SIZE = 8192
# Iterates over the IO, encoding and yielding individual chunks
# of the response entity.
# @yield [chunk]
# @yieldparam [String] chunk a chunk of the response, encoded
def each
while chunk = body.read(CHUNK_SIZE) and chunk != ""
yield resource.send(encoder, resource.send(charsetter, chunk))
end
end

# If IO#copy_stream is supported, and the stream is unencoded,
# optimize the output by copying directly. Otherwise, defers to
# using #each.
# @param [IO] outstream the output stream to copy the body into
def copy_stream(outstream)
if can_copy_stream?
IO.copy_stream(body, outstream)
else
each {|chunk| outstream << chunk }
end
end

# Allows the response body to be converted to a IO object.
# @return [IO,nil] the body as a IO object, or nil.
def to_io
IO.try_convert(body)
end

# Returns the length of the IO stream, if known. Returns nil if
# the stream uses an encoder or charsetter that might modify the
# length of the stream, or the stream size is unknown.
# @return [Integer] the size, in bytes, of the underlying IO, or
# nil if unsupported
def size
if is_unencoded?
if is_string_io?
body.size
else
begin
body.stat.size
rescue SystemCallError
# IO objects might raise an Errno if stat is unsupported.
nil
end
end
end
end

def empty?
size == 0
end

alias bytesize size

private
def can_copy_stream?
IO.respond_to?(:copy_stream) && is_unencoded? && !is_string_io?
end

def is_string_io?
StringIO === body
end
end # class IOEncoder
end # module Streaming
end # module Webmachine
@@ -0,0 +1,93 @@
require 'webmachine/events'
require 'webmachine/trace/resource_proxy'
require 'webmachine/trace/fsm'
require 'webmachine/trace/pstore_trace_store'
require 'webmachine/trace/trace_resource'
require 'webmachine/trace/listener'

module Webmachine
# Contains means to enable the Webmachine visual debugger.
module Trace
module_function
# Classes that implement storage for visual debugger traces.
TRACE_STORES = {
:memory => Hash,
:pstore => PStoreTraceStore
}

DEFAULT_TRACE_SUBSCRIBER = Webmachine::Events.subscribe(
/wm\.trace\..+/,
Webmachine::Trace::Listener.new
)

# Determines whether this resource instance should be traced.
# @param [Webmachine::Resource] resource a resource instance
# @return [true, false] whether to trace the resource
def trace?(resource)
# For now this defers to the resource to enable tracing in the
# initialize method. At a later time, we can add tracing at the
# Application level, perhaps.
resource.trace?
end

# Records a trace from a processed request. This is automatically
# called by {Webmachine::Trace::ResourceProxy} when finishing the
# request.
# @api private
# @param [String] key a unique identifier for the request
# @param [Array] trace the raw trace generated by processing the
# request
def record(key, trace)
trace_store[key] = trace
end

# Retrieves keys of traces that have been recorded. This is used
# to present a list of available traces in the visual debugger.
# @api private
# @return [Array<String>] a list of recorded traces
def traces
trace_store.keys
end

# Fetches a given trace from the trace store. This is used to
# send specific trace information to the visual debugger.
# @api private
# @param [String] key the trace's key
# @return [Array] the raw trace
def fetch(key)
trace_store.fetch(key)
end

# Sets the trace storage method. The first parameter should be a
# Symbol, followed by any additional options for the
# store. Defaults to :memory, which is an in-memory Hash.
# @example
# Webmachine::Trace.trace_store = :pstore, "/tmp/webmachine.trace"
def trace_store=(*args)
@trace_store = nil
@trace_store_opts = args
end
self.trace_store = :memory

def trace_store
@trace_store ||= begin
opts = Array(@trace_store_opts).dup
type = opts.shift
TRACE_STORES[type].new(*opts)
end
end
private :trace_store

# Sets the trace listener objects.
# Defaults to Webmachine::Trace::Listener.new.
# @param [Array<Object>] listeners a list of event listeners
# @return [Array<Object>] a list of event subscribers
def trace_listener=(listeners)
Webmachine::Events.unsubscribe(DEFAULT_TRACE_SUBSCRIBER)

Array(listeners).map do |listener|
Webmachine::Events.subscribe(/wm\.trace\..+/, listener)
end
end
end
end
@@ -0,0 +1,78 @@
module Webmachine
module Trace
# This module is injected into {Webmachine::Decision::FSM} when
# tracing is enabled for a resource, enabling the capturing of
# traces.
module FSM
# Overrides the default resource accessor so that incoming
# callbacks are traced.
def initialize(_resource, _request, _response)
if trace?
class << self
def resource
@resource_proxy ||= ResourceProxy.new(@resource)
end
end
end
end

def trace?
Trace.trace?(@resource)
end

# Adds the request to the trace.
# @param [Webmachine::Request] request the request to be traced
def trace_request(request)
response.trace << {
:type => :request,
:method => request.method,
:path => request.uri.request_uri.to_s,
:headers => request.headers,
:body => request.body.to_s
} if trace?
end

# Adds the response to the trace and then commits the trace to
# separate storage which can be discovered by the debugger.
# @param [Webmachine::Response] response the response to be traced
def trace_response(response)
response.trace << {
:type => :response,
:code => response.code.to_s,
:headers => response.headers,
:body => trace_response_body(response.body)
} if trace?
ensure
Webmachine::Events.publish('wm.trace.record', {
:trace_id => resource.object_id.to_s,
:trace => response.trace
}) if trace?
end

# Adds a decision to the trace.
# @param [Symbol] decision the decision being processed
def trace_decision(decision)
response.trace << {:type => :decision, :decision => decision} if trace?
end

private
# Works around streaming encoders where possible
def trace_response_body(body)
case body
when Streaming::FiberEncoder
# TODO: figure out how to properly rewind or replay the
# fiber
body.inspect
when Streaming::EnumerableEncoder
body.body.join
when Streaming::CallableEncoder
body.body.call.to_s
when Streaming::IOEncoder
body.body.inspect
else
body.to_s
end
end
end
end
end
@@ -0,0 +1,12 @@
module Webmachine
module Trace
class Listener
def call(name, payload)
key = payload.fetch(:trace_id)
trace = payload.fetch(:trace)

Webmachine::Trace.record(key, trace)
end
end
end
end
@@ -0,0 +1,39 @@
require 'pstore'

module Webmachine
module Trace
# Implements a trace storage using PStore from Ruby's standard
# library. To use this trace store, specify the :pstore engine
# and a path where it can store traces:
# @example
# Webmachine::Trace.trace_store = :pstore, "/tmp/webmachine.trace"
class PStoreTraceStore
# @api private
# @param [String] path where to store traces in a PStore
def initialize(path)
@pstore = PStore.new(path)
end

# Lists the recorded traces
# @api private
# @return [Array] a list of recorded traces
def keys
@pstore.transaction(true) { @pstore.roots }
end

# Fetches a trace from the store
# @api private
# @param [String] key the trace to fetch
# @return [Array] a raw trace
def fetch(key)
@pstore.transaction(true) { @pstore[key] }
end

# Records a trace in the store
# @api private
def []=(key, trace)
@pstore.transaction { @pstore[key] = trace }
end
end
end
end
@@ -0,0 +1,107 @@
require 'webmachine/resource'

module Webmachine
module Trace
# This class is injected into the decision FSM as a stand-in for
# the resource when tracing is enabled. It proxies all callbacks
# to the resource so that they get logged in the trace.
class ResourceProxy
# @return [Webmachine::Resource] the wrapped resource
attr_reader :resource

# Callback methods that can return data that refers to
# user-defined callbacks that are not in the canonical set,
# including body-producing or accepting methods, encoders and
# charsetters.
CALLBACK_REFERRERS = [:content_types_accepted, :content_types_provided,
:encodings_provided, :charsets_provided]

# Creates a {ResourceProxy} that decorates the passed
# {Webmachine::Resource} such that callbacks invoked by the
# {Webmachine::Decision::FSM} will be logged in the response's
# trace.
def initialize(resource)
@resource = resource
@dynamic_callbacks = Module.new
extend @dynamic_callbacks
end

# Create wrapper methods for every exposed callback
Webmachine::Resource::Callbacks.instance_methods(false).each do |c|
define_method c do |*args|
proxy_callback c, *args
end
end

def charset_nop(*args)
proxy_callback :charset_nop, *args
end

# Calls the resource's finish_request method and then sets the trace id
# header in the response.
def finish_request(*args)
proxy_callback :finish_request, *args
ensure
resource.response.headers['X-Webmachine-Trace-Id'] = object_id.to_s
end

private
# Proxy a given callback to the inner resource, decorating with traces
def proxy_callback(callback, *args)
# Log inputs and attempt
resource.response.trace << attempt(callback, args)
# Do the call
_result = resource.send(callback, *args)
add_dynamic_callback_proxies(_result) if CALLBACK_REFERRERS.include?(callback.to_sym)
resource.response.trace << result(_result)
_result
rescue => exc
exc.backtrace.reject! {|s| s.include?(__FILE__) }
resource.response.trace << exception(exc)
raise
end

# Creates a log entry for the entry to a resource callback.
def attempt(callback, args)
log = {:type => :attempt}
method = resource.method(callback)
if method.owner == ::Webmachine::Resource::Callbacks
log[:name] = "(default)##{method.name}"
else
log[:name] = "#{method.owner.name}##{method.name}"
log[:source] = method.source_location.join(":") if method.respond_to?(:source_location)
end
unless args.empty?
log[:args] = args
end
log
end

# Creates a log entry for the result of a resource callback
def result(result)
{:type => :result, :value => result}
end

# Creates a log entry for an exception that was raised from a callback
def exception(e)
{:type => :exception,
:class => e.class.name,
:backtrace => e.backtrace,
:message => e.message }
end

# Adds proxy methods for callbacks that are dynamically referred to.
def add_dynamic_callback_proxies(pairs)
pairs.to_a.each do |(_, m)|
unless respond_to?(m)
@dynamic_callbacks.module_eval do
define_method m do |*args|
proxy_callback m, *args
end
end
end
end
end
end
end
end
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,54 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Webmachine Trace <%= name %></title>
<link rel="stylesheet" type="text/css" href="static/wmtrace.css" />
<script type="text/javascript" src="static/wmtrace.js"></script>
<script type="text/javascript">
<!--
var request = <%= treq %>;
var response = <%= tres %>;
var trace = <%= trace %>
-->
</script>
</head>
<body>
<div id="zoompanel">
<button id="zoomout">zoom out</button>
<button id="zoomin">zoom in</button>
</div>
<canvas id="v3map" width="3138" height="2184"></canvas>
<div id="sizetest"></div>
<div id="preview">
<div id="previewid"></div>
<ul id="previewcalls"></ul>
</div>
<div id="infopanel">
<div id="infocontrols">
<div id="requesttab" class="selectedtab">Q</div>
<div id="responsetab">R</div>
<div id="decisiontab">D</div>
</div>
<div id="requestdetail">
<div>
<span id="requestmethod"></span> <span id="requestpath"></span>
</div>
<table id="requestheaders" class="headers"></table>
<pre id="requestbody"></pre>
</div>
<div id="responsedetail">
<div id="responsecode"></div>
<table id="responseheaders" class="headers"></table>
<pre id="responsebody"></pre>
</div>
<div id="decisiondetail">
<div>Decision: <select id="decisionid"></select></div>
<div>Calls: <select id="decisioncalls"></select></div>
<div>Input:</div>
<pre id="callinput"></pre>
<div>Output:</div>
<pre id="calloutput"></pre>
</div>
</div>
</body>
</html>
@@ -0,0 +1,14 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Webmachine Trace List</title>
</head>
<body>
<h1>Traces</h1>
<ul>
<% traces.each do |(trace, uri)| %>
<li><a href="<%= uri %>"><%= trace %></a></li>
<% end %>
</ul>
</body>
</html>
@@ -0,0 +1,123 @@
body {
margin:0px;
padding:0px;
}

canvas#v3map {
margin-top:2em;
z-index: 1;
}

div#sizetest {
width:100%;
}

div#zoompanel {
height:2em;
position:fixed;
z-index:10;
}

div#preview {
position:absolute;
display:none;
background:#dddddd;
border:1px solid #999999;
}

div#preview ul {
padding: 0px 0px 0px 0.5em;
margin: 0px;
list-style: none;
}

div#infopanel {
z-index:20;
background:#dddddd;
position:fixed;
top:0px;
right:0px;
bottom:0px;
left:75%;
min-width:30em;
padding:5px;
}

div#infocontrols {
position:absolute;
top:0px;
bottom:0px;
left:-5px;
width:5px;
background:#999999;
cursor:ew-resize;
}

div#infocontrols div {
position:absolute;
left:-15px;
width:20px;
height:49px;
background:#999999;
cursor:pointer;
}

div#infocontrols div.selectedtab {
background:#dddddd;
border-top: 1px solid #999999;
border-left: 1px solid #999999;
border-bottom: 1px solid #999999;
}

div#requesttab {
top:2px;
}

div#responsetab {
top:54px;
}

div#decisiontab {
top:106px;
}

div#requestdetail, div#responsedetail, div#decisiondetail {
height:100%;
}

div#responsedetail, div#decisiondetail {
display:none;
}

div#infopanel ul {
list-style:none;
padding-left:0px;
height:5em;
overflow-y:scroll;
}

pre {
height:40%;
overflow: auto;
}

table.headers {
border-collapse: collapse;
margin: 10px 20px 10px 0;
}

table.headers th {
text-align: right ! important;
text-transform: capitalize;
white-space: nowrap;
vertical-align: top;
}

table.headers td, table.headers th {
padding: 5px 3px;
}

div#responsebody, div#requestbody {
height:70%;
overflow-y:scroll;
}