Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'originalmachine-reel' into reel

Conflicts:
	webmachine.gemspec
  • Loading branch information...
commit 5f26d0904abc2cee0f225a32dada33c74277166f 2 parents 0011488 + be7cf45
@tarcieri tarcieri authored
Showing with 1,877 additions and 140 deletions.
  1. +70 −7 README.md
  2. +19 −0 Rakefile
  3. +32 −0 examples/debugger.rb
  4. +6 −2 examples/webrick.rb
  5. +1 −0  lib/webmachine.rb
  6. +1 −0  lib/webmachine/adapters.rb
  7. +16 −3 lib/webmachine/adapters/rack.rb
  8. +10 −10 lib/webmachine/application.rb
  9. +1 −1  lib/webmachine/decision/flow.rb
  10. +16 −4 lib/webmachine/decision/fsm.rb
  11. +25 −1 lib/webmachine/decision/helpers.rb
  12. +34 −5 lib/webmachine/dispatcher.rb
  13. +2 −0  lib/webmachine/dispatcher/route.rb
  14. +3 −3 lib/webmachine/media_type.rb
  15. +3 −1 lib/webmachine/resource.rb
  16. +1 −1  lib/webmachine/resource/authentication.rb
  17. +1 −1  lib/webmachine/resource/callbacks.rb
  18. +20 −0 lib/webmachine/resource/tracing.rb
  19. +2 −6 lib/webmachine/response.rb
  20. +74 −0 lib/webmachine/trace.rb
  21. +60 −0 lib/webmachine/trace/fsm.rb
  22. +39 −0 lib/webmachine/trace/pstore_trace_store.rb
  23. +107 −0 lib/webmachine/trace/resource_proxy.rb
  24. BIN  lib/webmachine/trace/static/http-headers-status-v3.png
  25. +54 −0 lib/webmachine/trace/static/trace.erb
  26. +14 −0 lib/webmachine/trace/static/tracelist.erb
  27. +123 −0 lib/webmachine/trace/static/wmtrace.css
  28. +725 −0 lib/webmachine/trace/static/wmtrace.js
  29. +129 −0 lib/webmachine/trace/trace_resource.rb
  30. +1 −1  lib/webmachine/version.rb
  31. +19 −0 spec/spec_helper.rb
  32. +66 −47 spec/webmachine/adapters/rack_spec.rb
  33. +21 −0 spec/webmachine/adapters/reel_spec.rb
  34. +1 −1  spec/webmachine/configuration_spec.rb
  35. +10 −10 spec/webmachine/cookie_spec.rb
  36. +3 −9 spec/webmachine/decision/conneg_spec.rb
  37. +8 −14 spec/webmachine/decision/flow_spec.rb
  38. +36 −6 spec/webmachine/decision/helpers_spec.rb
  39. +1 −1  spec/webmachine/dispatcher_spec.rb
  40. +1 −1  spec/webmachine/headers_spec.rb
  41. +1 −1  spec/webmachine/media_type_spec.rb
  42. +3 −3 spec/webmachine/resource/authentication_spec.rb
  43. +32 −0 spec/webmachine/trace/fsm_spec.rb
  44. +36 −0 spec/webmachine/trace/resource_proxy_spec.rb
  45. +29 −0 spec/webmachine/trace/trace_store_spec.rb
  46. +17 −0 spec/webmachine/trace_spec.rb
  47. +4 −1 webmachine.gemspec
View
77 README.md
@@ -28,7 +28,7 @@ application for it!
```ruby
require 'webmachine'
# Require any of the files that contain your resources here
-require 'my_resource'
+require 'my_resource'
# Create an application which encompasses routes and configruation
MyApp = Webmachine::Application.new do |app|
@@ -63,7 +63,7 @@ class MyResource < Webmachine::Resource
def encodings_provided
{"gzip" => :encode_gzip, "identity" => :encode_identity}
end
-
+
def to_html
"<html><body>Hello, world!</body></html>"
end
@@ -85,7 +85,7 @@ object, `Webmachine.application` will return a global one.
```ruby
require 'webmachine'
require 'my_resource'
-
+
Webmachine.application.routes do
add ['*'], MyResource
end
@@ -95,11 +95,52 @@ Webmachine.application.configure do |config|
config.port = 3000
config.adapter = :Mongrel
end
-
+
# Start the server.
Webmachine.application.run
```
+### Visual debugger
+
+It can be hard to understand all of the decisions that Webmachine
+makes when servicing a request to your resource, which is why we have
+the "visual debugger". In development, you can turn on tracing of the
+decision graph for a resource by implementing the `#trace?` callback
+so that it returns true:
+
+```ruby
+class MyTracedResource < Webmachine::Resource
+ def trace?
+ true
+ end
+
+ # The rest of your callbacks...
+end
+```
+
+Then enable the visual debugger resource by adding a route to your
+configuration:
+
+```ruby
+Webmachine.application.routes do
+ # This can be any path as long as it ends with '*'
+ add ['trace', '*'], Webmachine::Trace::TraceResource
+ # The rest of your routes...
+end
+```
+
+Now when you visit your traced resource, a trace of the request
+process will be recorded in memory. Open your browser to `/trace` to
+list the recorded traces and inspect the result. The response from your
+traced resource will also include the `X-Webmachine-Trace-Id` that you
+can use to lookup the trace. It might look something like this:
+
+![preview calls at decision](http://seancribbs-skitch.s3.amazonaws.com/Webmachine_Trace_2156885920-20120625-100153.png)
+
+Refer to
+[examples/debugger.rb](/seancribbs/webmachine-ruby/blob/master/examples/debugger.rb)
+for an example of how to enable the debugger.
+
## Features
* Handles the hard parts of content negotiation, conditional
@@ -112,13 +153,12 @@ Webmachine.application.run
* Streaming/chunked response bodies are permitted as Enumerables,
Procs, or Fibers!
* Unlike the Erlang original, it does real Language negotiation.
+* Includes the visual debugger so you can look through the decision
+ graph to determine how your resources are behaving.
## Problems/TODOs
* Command-line tools, and general polish.
-* Tracing is exposed as an Array of decisions visited on the response
- object. You should be able to turn this off and on, and visualize
- the decisions on the sequence diagram.
## LICENSE
@@ -128,6 +168,29 @@ LICENSE for details.
## Changelog
+### 1.0.0 July 7, 2012
+
+1.0.0 is a major feature release that finally includes the visual
+debugger, some nice cookie support, and some new extension
+points. Added Peter Johanson and Armin Joellenbeck as
+contributors. Thank you for your contributions!
+
+* A cookie parsing and manipulation API was added.
+* Conneg headers now accept any amount of whitespace around commas,
+ including none.
+* `Callbacks#handle_exception` was added so that resources can handle
+ exceptions that they generate and produce more friendly responses.
+* Chunked and non-chunked response bodies in the Rack adapter were
+ fixed.
+* The WEBrick example was updated to use the new API.
+* `Dispatcher` was refactored so that you can modify how resources
+ are initialized before dispatching occurs.
+* `Route` now includes the `Translation` module so that exception
+ messages are properly rendered.
+* The visual debugger was added (more details in the README).
+* The `Content-Length` header will always be set inside Webmachine and
+ is no longer reliant on the adapter to set it.
+
### 0.4.2 March 22, 2012
0.4.2 is a bugfix release that corrects a few minor issues. Added Lars
View
19 Rakefile
@@ -32,6 +32,25 @@ task :release => :gem do
system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
end
+desc "Cleans up white space in source files"
+task :clean_whitespace do
+ no_file_cleaned = true
+
+ Dir["**/*.rb"].each do |file|
+ contents = File.read(file)
+ cleaned_contents = contents.gsub(/([ \t]+)$/, '')
+ unless cleaned_contents == contents
+ no_file_cleaned = false
+ puts " - Cleaned #{file}"
+ File.open(file, 'w') { |f| f.write(cleaned_contents) }
+ end
+ end
+
+ if no_file_cleaned
+ puts "No files with trailing whitespace found"
+ end
+end
+
require 'rspec/core'
require 'rspec/core/rake_task'
View
32 examples/debugger.rb
@@ -0,0 +1,32 @@
+require 'webmachine'
+require 'webmachine/trace'
+
+class MyTracedResource < Webmachine::Resource
+ def trace?; true; end
+
+ def resource_exists?
+ case request.query['e']
+ when 'true'
+ true
+ when 'fail'
+ raise "BOOM"
+ else
+ false
+ end
+ end
+
+ def to_html
+ "<html>You found me.</html>"
+ end
+end
+
+# Webmachine::Trace.trace_store = :pstore, "./trace"
+
+TraceExample = Webmachine::Application.new do |app|
+ app.routes do
+ add ['trace', '*'], Webmachine::Trace::TraceResource
+ add [], MyTracedResource
+ end
+end
+
+TraceExample.run
View
8 examples/webrick.rb
@@ -14,6 +14,10 @@ def to_html
end
end
-Webmachine::Dispatcher.add_route([], HelloResource)
+App = Webmachine::Application.new do |app|
+ app.routes do
+ add [], HelloResource
+ end
+end
-Webmachine.run
+App.run
View
1  lib/webmachine.rb
@@ -11,6 +11,7 @@
require 'webmachine/dispatcher'
require 'webmachine/application'
require 'webmachine/resource'
+require 'webmachine/trace'
require 'webmachine/version'
# Webmachine is a toolkit for making well-behaved HTTP applications.
View
1  lib/webmachine/adapters.rb
@@ -6,5 +6,6 @@ module Webmachine
# application servers.
module Adapters
autoload :Mongrel, 'webmachine/adapters/mongrel'
+ autoload :Reel, 'webmachine/adapters/reel'
end
end
View
19 lib/webmachine/adapters/rack.rb
@@ -4,6 +4,7 @@
require 'webmachine/request'
require 'webmachine/response'
require 'webmachine/dispatcher'
+require 'webmachine/chunked_body'
module Webmachine
module Adapters
@@ -58,10 +59,22 @@ def call(env)
response.headers['Server'] = [Webmachine::SERVER_STRING, "Rack/#{::Rack.version}"].join(" ")
- body = response.body.respond_to?(:call) ? response.body.call : response.body
- body = body.is_a?(String) ? [ body ] : body
+ rack_status = response.code
+ rack_headers = response.headers.flattened("\n")
+ rack_body = case response.body
+ when String # Strings are enumerable in ruby 1.8
+ [response.body]
+ else
+ if response.body.respond_to?(:call)
+ Webmachine::ChunkedBody.new(Array(response.body.call))
+ elsif response.body.respond_to?(:each)
+ Webmachine::ChunkedBody.new(response.body)
+ else
+ [response.body.to_s]
+ end
+ end
- [response.code.to_i, response.headers.flattened("\n"), body || []]
+ [rack_status, rack_headers, rack_body]
end
# Wraps the Rack input so it can be treated like a String or
View
20 lib/webmachine/application.rb
@@ -4,19 +4,19 @@
module Webmachine
# How to get your Webmachine app running:
- #
+ #
# MyApp = Webmachine::Application.new do |app|
# app.routes do
# add ['*'], AssetResource
# end
- #
+ #
# app.configure do |config|
# config.port = 8888
# end
# end
- #
+ #
# MyApp.run
- #
+ #
class Application
extend Forwardable
@@ -32,17 +32,17 @@ class Application
#
# An instance of application contains Adapter configuration and
# a Dispatcher instance which can be configured with Routes.
- #
+ #
# @param [Webmachine::Configuration] configuration
# a Webmachine::Configuration
- #
+ #
# @yield [app]
# a block in which to configure this Application
# @yieldparam [Application]
# the Application instance being initialized
- def initialize(configuration = Configuration.default)
+ def initialize(configuration = Configuration.default, dispatcher = Dispatcher.new)
@configuration = configuration
- @dispatcher = Dispatcher.new
+ @dispatcher = dispatcher
yield self if block_given?
end
@@ -66,10 +66,10 @@ def adapter_class
# Evaluates the passed block in the context of {Webmachine::Dispatcher}
# for use in adding a number of routes at once.
- #
+ #
# @return [Application, Array<Route>]
# self if configuring, or an Array of Routes otherwise
- #
+ #
# @see Webmachine::Dispatcher#add_route
def routes(&block)
if block_given?
View
2  lib/webmachine/decision/flow.rb
@@ -8,7 +8,7 @@ module Decision
# This module encapsulates all of the decisions in Webmachine's
# flow-chart. These invoke {Webmachine::Resource::Callbacks} methods to
# determine the appropriate response code, headers, and body for
- # the response.
+ # the response.
#
# This module is included into {FSM}, which drives the processing
# of the chart.
View
20 lib/webmachine/decision/fsm.rb
@@ -16,13 +16,15 @@ class FSM
def initialize(resource, request, response)
@resource, @request, @response = resource, request, response
@metadata = {}
+ initialize_tracing
end
# Processes the request, iteratively invoking the decision methods in {Flow}.
def run
state = Flow::START
+ trace_request(request)
loop do
- response.trace << state
+ trace_decision(state)
result = send(state)
case result
when Fixnum # Response code
@@ -38,9 +40,8 @@ def run
Webmachine.render_error(400, request, response, :message => malformed.message)
respond(400)
rescue Exception => e # Handle all exceptions without crashing the server
- response.end_state = state
code = resource.handle_exception(e)
- code = (100...600).include?(code) ? (code) : (500)
+ code = (100...600).include?(code) ? (code) : (500)
respond(code)
end
@@ -57,9 +58,20 @@ def respond(code, headers={})
end
response.code = code
resource.finish_request
- # TODO: add logging/tracing
+ ensure_content_length
+ trace_response(response)
end
+ # When tracing is disabled, this does nothing.
+ def trace_decision(state); end
+ # When tracing is disabled, this does nothing.
+ def trace_request(request); end
+ # When tracing is disabled, this does nothing.
+ def trace_response(response); end
+
+ def initialize_tracing
+ extend Trace::FSM if Trace.trace?(resource)
+ end
end # class FSM
end # module Decision
end # module Webmachine
View
26 lib/webmachine/decision/helpers.rb
@@ -41,7 +41,7 @@ def encode_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
+ set_content_length
else
response.headers.delete 'Content-Length'
response.headers['Transfer-Encoding'] = 'chunked'
@@ -99,6 +99,30 @@ def add_caching_headers
response.headers['Last-Modified'] = modified.httpdate
end
end
+
+ # Ensures that responses have an appropriate Content-Length
+ # header
+ def ensure_content_length
+ case
+ when response.headers['Transfer-Encoding']
+ return
+ when [204, 205, 304].include?(response.code)
+ response.headers.delete 'Content-Length'
+ when has_response_body?
+ set_content_length
+ else
+ response.headers['Content-Length'] = '0'
+ end
+ end
+
+ # Sets the Content-Length header on the response
+ def set_content_length
+ if response.body.respond_to?(:bytesize)
+ response.headers['Content-Length'] = response.body.bytesize.to_s
+ else
+ response.headers['Content-Length'] = response.body.length.to_s
+ end
+ end
end # module Helpers
end # module Decision
end # module Webmachine
View
39 lib/webmachine/dispatcher.rb
@@ -11,9 +11,16 @@ class Dispatcher
# @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,10 +39,7 @@ 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
- route.apply(request)
- resource = route.resource.new(request, response)
+ if resource = find_resource(request, response)
Webmachine::Decision::FSM.new(resource, request, response).run
else
Webmachine.render_error(404, request, response)
@@ -47,6 +51,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
View
2  lib/webmachine/dispatcher/route.rb
@@ -6,6 +6,8 @@ class Dispatcher
# Pairs URIs with {Resource} classes in the {Dispatcher}. To
# create routes, use {Dispatcher#add_route}.
class Route
+ include Webmachine::Translation
+
# @return [Class] the resource this route will dispatch to, a
# subclass of {Resource}
attr_reader :resource
View
6 lib/webmachine/media_type.rb
@@ -62,7 +62,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 +73,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 +88,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
View
4 lib/webmachine/resource.rb
@@ -1,6 +1,7 @@
require 'webmachine/resource/callbacks'
require 'webmachine/resource/encodings'
require 'webmachine/resource/authentication'
+require 'webmachine/resource/tracing'
module Webmachine
# Resource is the primary building block of Webmachine applications,
@@ -21,6 +22,7 @@ module Webmachine
class Resource
include Callbacks
include Encodings
+ include Tracing
attr_reader :request, :response
@@ -38,7 +40,7 @@ def self.new(request, response)
instance.send :initialize
instance
end
-
+
private
# When no specific charsets are provided, this acts as an identity
# on the response body. Probably deserves some refactoring.
View
2  lib/webmachine/resource/authentication.rb
@@ -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
View
2  lib/webmachine/resource/callbacks.rb
@@ -361,7 +361,7 @@ def generate_etag
def finish_request; end
#
- # This method is called when an exception is raised within a subclass of
+ # This method is called when an exception is raised within a subclass of
# {Webmachine::Resource}.
#
# @param [Exception] e
View
20 lib/webmachine/resource/tracing.rb
@@ -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
View
8 lib/webmachine/response.rb
@@ -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 = HeaderHash.new
@trace = []
self.code = 200
- self.redirect = false
+ self.redirect = false
end
# Indicate that the response should be a redirect. This is only
View
74 lib/webmachine/trace.rb
@@ -0,0 +1,74 @@
+require 'webmachine/trace/resource_proxy'
+require 'webmachine/trace/fsm'
+require 'webmachine/trace/pstore_trace_store'
+require 'webmachine/trace/trace_resource'
+
+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
+ }
+
+ # 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
+ end
+end
View
60 lib/webmachine/trace/fsm.rb
@@ -0,0 +1,60 @@
+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
+ # 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
+ }
+ end
+
+ # Adds the response to the trace.
+ # @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)
+ }
+ end
+
+ # Adds a decision to the trace.
+ # @param [Symbol] decision the decision being processed
+ def trace_decision(decision)
+ response.trace << {:type => :decision, :decision => decision}
+ end
+
+ # Overrides the default resource accessor so that incoming
+ # callbacks are traced.
+ def resource
+ @resource_proxy ||= ResourceProxy.new(@resource)
+ end
+
+ private
+ # Works around streaming encoders where possible
+ def trace_response_body(body)
+ case body
+ when FiberEncoder
+ # TODO: figure out how to properly rewind or replay the
+ # fiber
+ body.inspect
+ when EnumerableEncoder
+ body.body.join
+ when CallableEncoder
+ body.body.call.to_s
+ else
+ body.to_s
+ end
+ end
+ end
+ end
+end
View
39 lib/webmachine/trace/pstore_trace_store.rb
@@ -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
View
107 lib/webmachine/trace/resource_proxy.rb
@@ -0,0 +1,107 @@
+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 commits
+ # the trace to separate storage which can be discovered by the
+ # debugger.
+ def finish_request(*args)
+ proxy_callback :finish_request, *args
+ ensure
+ resource.response.headers['X-Webmachine-Trace-Id'] = object_id.to_s
+ Trace.record(object_id.to_s, resource.response.trace)
+ 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
View
BIN  lib/webmachine/trace/static/http-headers-status-v3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
54 lib/webmachine/trace/static/trace.erb
@@ -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>
View
14 lib/webmachine/trace/static/tracelist.erb
@@ -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| %>
+ <li><a href="<%= trace %>"><%= trace %></a></li>
+ <% end %>
+ </ul>
+ </body>
+</html>
View
123 lib/webmachine/trace/static/wmtrace.css
@@ -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;
+}
View
725 lib/webmachine/trace/static/wmtrace.js
@@ -0,0 +1,725 @@
+var HIGHLIGHT = '#cc00cc';
+var REGULAR = '#666666';
+
+var cols = {
+ 'a':173,
+ 'b':325,
+ 'c':589,
+ 'd':797,
+ 'e':1005,
+ 'f':1195,
+ 'g':1402,
+ 'gg':1515,
+ 'h':1572,
+ 'i':1799,
+ 'j':1893,
+ 'k':1988,
+ 'l':2157,
+ 'll':2346,
+ 'm':2403,
+ 'mm':2535,
+ 'n':2554,
+ 'o':2649,
+ 'oo':2781,
+ 'ooo':2801,
+ 'p':2894,
+ 'q':3007
+};
+
+var rows = {
+ '1':221,
+ '2':298,
+ '3':373,
+ '4':448,
+ '5':524,
+ '6':599,
+ '7':675,
+ '8':751,
+ '9':826,
+ '10':902,
+ '11':977,
+ '12':1053,
+ '13':1129,
+ '14':1204,
+ '15':1280,
+ '16':1355,
+ '17':1431,
+ '18':1506,
+ '19':1583,
+ '20':1658,
+ '21':1734,
+ '22':1809,
+ '23':1885,
+ '24':1961,
+ '25':2036,
+ '26':2112
+};
+
+var edges = {
+ 'b14b13':['b14','b13'],
+
+ 'b13b12':['b13','b12'],
+ 'b13503':['b13','503'],
+
+ 'b12b11':['b12','b11'],
+ 'b12501':['b12','501'],
+
+ 'b11b10':['b11','b10'],
+ 'b11414':['b11','414'],
+
+ 'b10b9':['b10','b9'],
+ 'b10405':['b10','405'],
+
+ 'b9b8':['b9','b8'],
+ 'b9400':['b9','400'],
+
+ 'b8b7':['b8','b7'],
+ 'b8401':['b8','401'],
+
+ 'b7b6':['b7','b6'],
+ 'b7403':['b7','403'],
+
+ 'b6b5':['b6','b5'],
+ 'b6501':['b6','501a'],
+
+ 'b5b4':['b5','b4'],
+ 'b5415':['b5','415'],
+
+ 'b4b3':['b4','b3'],
+ 'b4413':['b4','b4'],
+
+ 'b3c3':['b3','c3'],
+ 'b3200':['b3','200'],
+
+ 'c3c4':['c3','c4'],
+ 'c3d4':['c3','d3','d4'],
+
+ 'c4d4':['c4','d4'],
+ 'c4406':['c4','406'],
+
+ 'd4d5':['d4','d5'],
+ 'd4e5':['d4','e4','e5'],
+
+ 'd5e5':['d5','e5'],
+ 'd5406':['d5','d7','406'],
+
+ 'e5e6':['e5','e6'],
+ 'e5f6':['e5','f5','f6'],
+
+ 'e6f6':['e6','f6'],
+ 'e6406':['e6','e7','406'],
+
+ 'f6f7':['f6','f7'],
+ 'f6g7':['f6','g6','g7'],
+
+ 'f7g7':['f7','g7'],
+ 'f7406':['f7','406'],
+
+ 'g7g8':['g7','g8'],
+ 'g7h7':['g7','h7'],
+
+ 'g8g9':['g8','g9'],
+ 'g8h10':['g8','h8','h10'],
+
+ 'g9g11':['g9','g11'],
+ 'g9h10':['g9','gg9','gg10','h10'],
+
+ 'g11h10':['g11','gg11','gg10','h10'],
+ 'g11412':['g11','g18','412a'],
+
+ 'h7i7':['h7','i7'],
+ 'h7412':['h7','412'],
+
+ 'h10h11':['h10','h11'],
+ 'h10i12':['h10','i10','i12'],
+
+ 'h11h12':['h11','h12'],
+ 'h11i12':['h11','i11','i12'],
+
+ 'h12i12':['h12','i12'],
+ 'h12412':['h12','412a'],
+
+ 'i4p3':['i4','i3','p3'],
+ 'i4301':['i4','301'],
+
+ 'i7i4':['i7','i4'],
+ 'i7k7':['i7','k7'],
+
+ 'i12l13':['i12','l12','l13'],
+ 'i12i13':['i12','i13'],
+
+ 'i13k13':['i13','k13'],
+ 'i13j18':['i13','i17','j17','j18'],
+
+ 'j18412':['j18','412a'],
+ 'j18304':['j18','304'],
+
+ 'k5l5':['k5','l5'],
+ 'k5301':['k5','301'],
+
+ 'k7k5':['k7','k5'],
+ 'k7l7':['k7','l7'],
+
+ 'k13j18':['k13','k17','j17','j18'],
+ 'k13l13':['k13','l13'],
+
+ 'l5m5':['l5','m5'],
+ 'l5307':['l5','307'],
+
+ 'l7m7':['l7','m7'],
+ 'l7404':['l7','l8','404'],
+
+ 'l13l14':['l13','l14'],
+ 'l13m16':['l13','m13','m16'],
+
+ 'l14l15':['l14','l15'],
+ 'l14m16':['l14','m14','m16'],
+
+ 'l15l17':['l15','l17'],
+ 'l15m16':['l15','ll15','ll16','m16'],
+
+ 'l17m16':['l17','ll17','ll16','m16'],
+ 'l17304':['l17','304'],
+
+ 'm5n5':['m5','n5'],
+ 'm5410':['m5','m4','410'],
+
+ 'm7n11':['m7','n7','n11'],
+ 'm7404':['m7','404'],
+
+ 'm16m20':['m16','m20'],
+ 'm16n16':['m16','n16'],
+
+ 'm20o20':['m20','o20'],
+ 'm20202':['m20','202'],
+
+ 'n5n11':['n5','n11'],
+ 'n5410':['n5','410'],
+
+ 'n11p11':['n11','p11'],
+ 'n11303':['n11','303'],
+
+ 'n16n11':['n16','n11'],
+ 'n16o16':['n16','o16'],
+
+ 'o14p11':['o14','o11','p11'],
+ 'o14409':['o14','409a'],
+
+ 'o16o14':['o16','o14'],
+ 'o16o18':['o16','o18'],
+
+ 'o18200':['o18','200a'],
+ 'o18300':['o18','oo18','300'],
+
+ 'o20o18':['o20','o18'],
+ 'o20204':['o20','204'],
+
+ 'p3p11':['p3','p11'],
+ 'p3409':['p3','409'],
+
+ 'p11o20':['p11','p20','o20'],
+ 'p11201':['p11','q11','201']
+};
+
+var ends = {
+ '200': {col:'a', row:'3', width:190},
+ '200a': {col:'mm', row:'18', width:116},
+ '201': {col:'q', row:'12', width:154},
+ '202': {col:'m', row:'21', width:116},
+ '204': {col:'o', row:'21', width:152},
+
+ '300': {col:'oo', row:'19', width:152},
+ '301': {col:'k', row:'4', width:154},
+ '303': {col:'m', row:'11', width:116},
+ '304': {col:'l', row:'18', width:116},
+ '307': {col:'l', row:'4', width:154},
+
+ '400': {col:'a', row:'9', width:190},
+ '401': {col:'a', row:'8', width:190},
+ '403': {col:'a', row:'7', width:190},
+ '404': {col:'m', row:'8', width:116},
+ '405': {col:'a', row:'10', width:190},
+ '406': {col:'c', row:'7', width:152},
+ '409': {col:'p', row:'2', width:116},
+ '409a': {col:'oo', row:'14', width:116},
+ '410': {col:'n', row:'4', width:116},
+ '412': {col:'h', row:'6', width:152},
+ '412a': {col:'h', row:'18', width:152},
+ '413': {col:'a', row:'4', width:190},
+ '414': {col:'a', row:'11', width:190},
+ '415': {col:'a', row:'5', width:190},
+
+ '501a': {col:'a', row:'6', width:190},
+ '501': {col:'a', row:'12', width:190},
+ '503': {col:'a', row:'13', width:190}
+};
+
+var canvas;
+
+function decorateTrace() {
+ trace[0].x = cols[trace[0].d[0]];
+ trace[0].y = rows[trace[0].d.slice(1)];
+ trace[0].previewCalls = previewCalls(trace[0]);
+
+ for (var i = 1; i < trace.length; i++) {
+ trace[i].x = cols[trace[i].d[0]];
+ trace[i].y = rows[trace[i].d.slice(1)];
+ trace[i].previewCalls = previewCalls(trace[i]);
+
+ var path = edges[trace[i-1].d+trace[i].d];
+ if (path) {
+ trace[i].path = [path.length-1];
+ for (var p = 1; p < path.length; p++) {
+ trace[i].path[p-1] = getSeg(path[p-1], path[p], p == path.length-1);
+ }
+ } else {
+ trace[i].path = [];
+ }
+ }
+
+ var path = edges[trace[i-1].d+response.code];
+ if (path) {
+ var end = ends[path[path.length-1]];
+ response.x = cols[end.col];
+ response.y = rows[end.row];
+ response.width = end.width;
+ response.type = 'normal';
+
+ response.path = [path.length-1];
+ for (var p = 1; p < path.length; p++) {
+ response.path[p-1] = getSeg(path[p-1], path[p], p == path.length-1);
+ }
+ } else {
+ var ld = trace[trace.length-1];
+ response.x = ld.x+50;
+ response.y = ld.y-50;
+ response.width = 38;
+ response.type = 'other';
+
+ response.path = [
+ {x1: ld.x+10, y1: ld.y-10,
+ x2: ld.x+36, y2: ld.y-36}
+ ];
+ }
+};
+
+function previewCalls(dec) {
+ var prev = '';
+ for (var i = 0; i < dec.calls.length; i++) {
+ if(dec.calls[i].call.indexOf("(default)") !== 0) {
+ prev += '<li>'+dec.calls[i].call;
+ if(dec.calls[i].source !== null)
+ prev += " (" + dec.calls[i].source + ")";
+ prev+='</li>';
+ }
+ }
+ return prev;
+};
+
+function drawTrace() {
+ drawDecision(trace[0]);
+ for (var i = 1; i < trace.length; i++) {
+ drawPath(trace[i].path);
+ drawDecision(trace[i]);
+ }
+
+ drawPath(response.path);
+ drawResponse();
+};
+
+function drawResponse() {
+ if (response.type == 'normal') {
+ var context = canvas.getContext('2d');
+ context.strokeStyle=HIGHLIGHT;
+ context.lineWidth=4;
+
+ context.beginPath();
+ context.rect(response.x-(response.width/2),
+ response.y-19,
+ response.width,
+ 38);
+ context.stroke();
+ } else {
+ var context = canvas.getContext('2d');
+ context.strokeStyle='#ff0000';
+ context.lineWidth=4;
+
+ context.beginPath();
+ context.arc(response.x, response.y, 19,
+ 0, 2*3.14159, false);
+ context.stroke();
+
+ }
+};
+
+function drawDecision(dec) {
+ var context = canvas.getContext('2d');
+
+ if (dec.previewCalls == '')
+ context.strokeStyle=REGULAR;
+ else
+ context.strokeStyle=HIGHLIGHT;
+ context.lineWidth=4;
+
+ context.beginPath();
+ context.moveTo(dec.x, dec.y-19);
+ context.lineTo(dec.x+19, dec.y);
+ context.lineTo(dec.x, dec.y+19);
+ context.lineTo(dec.x-19, dec.y);
+ context.closePath();
+ context.stroke();
+};
+
+function drawPath(path) {
+ var context = canvas.getContext('2d');
+ context.strokeStyle=REGULAR;
+ context.lineWidth=4;
+
+ context.beginPath();
+ context.moveTo(path[0].x1, path[0].y1);
+ for (var p = 0; p < path.length; p++) {
+ context.lineTo(path[p].x2, path[p].y2);
+ }
+ context.stroke();
+};
+
+function getSeg(p1, p2, last) {
+ var seg = {
+ x1:cols[p1[0]],
+ y1:rows[p1.slice(1)]
+ };
+ if (ends[p2]) {
+ seg.x2 = cols[ends[p2].col];
+ seg.y2 = rows[ends[p2].row];
+ } else {
+ seg.x2 = cols[p2[0]];
+ seg.y2 = rows[p2.slice(1)];
+ }
+
+ if (seg.x1 == seg.x2) {
+ if (seg.y1 < seg.y2) {
+ seg.y1 = seg.y1+19;
+ if (last) seg.y2 = seg.y2-19;
+ } else {
+ seg.y1 = seg.y1-19;
+ if (last) seg.y2 = seg.y2+19;
+ }
+ } else {
+ //assume seg.y1 == seg.y2
+ if (seg.x1 < seg.x2) {
+ seg.x1 = seg.x1+19;
+ if (last) seg.x2 = seg.x2-(ends[p2] ? (ends[p2].width/2) : 19);
+ } else {
+ seg.x1 = seg.x1-19;
+ if (last) seg.x2 = seg.x2+(ends[p2] ? (ends[p2].width/2) : 19);
+ }
+ }
+ return seg;
+};
+
+function traceDecision(name) {
+ for (var i = trace.length-1; i >= 0; i--)
+ if (trace[i].d == name) return trace[i];
+};
+
+var detailPanels = {};
+function initDetailPanels() {
+ var windowWidth = document.getElementById('sizetest').clientWidth;
+ var infoPanel = document.getElementById('infopanel');
+ var panelWidth = windowWidth-infoPanel.offsetLeft;
+
+ var panels = {
+ 'request': document.getElementById('requestdetail'),
+ 'response': document.getElementById('responsedetail'),
+ 'decision': document.getElementById('decisiondetail')
+ };
+
+ var tabs = {
+ 'request': document.getElementById('requesttab'),
+ 'response': document.getElementById('responsetab'),
+ 'decision': document.getElementById('decisiontab')
+ };
+
+ var decisionId = document.getElementById('decisionid');
+ var decisionCalls = document.getElementById('decisioncalls');
+ var callInput = document.getElementById('callinput');
+ var callOutput = document.getElementById('calloutput');
+
+ var lastUsedPanelWidth = windowWidth-infoPanel.offsetLeft;
+
+ var setPanelWidth = function(width) {
+ infoPanel.style.left = (windowWidth-width)+'px';
+ canvas.style.marginRight = (width+20)+'px';
+ panelWidth = width;
+ };
+ setPanelWidth(panelWidth);
+
+ var ensureVisible = function() {
+ if (windowWidth-infoPanel.offsetLeft < 10)
+ setPanelWidth(lastUsedPanelWidth);
+ };
+
+ var decChoices = '';
+ for (var i = 0; i < trace.length; i++) {
+ decChoices += '<option value="'+trace[i].d+'">'+trace[i].d+'</option>';
+ }
+ decisionId.innerHTML = decChoices;
+ decisionId.selectedIndex = -1;
+
+ decisionId.onchange = function() {
+ detailPanels.setDecision(traceDecision(decisionId.value));
+ }
+
+ detailPanels.setDecision = function(dec) {
+ decisionId.value = dec.d;
+
+ var calls = [];
+ for (var i = 0; i < dec.calls.length; i++) {
+ calls.push('<option value="'+dec.d+'-'+i+'">');
+ calls.push(dec.calls[i].call);
+ if(dec.calls[i].source !== null)
+ calls.push(' (' + dec.calls[i].source + ')');
+ calls.push('</option>');
+ }
+ decisionCalls.innerHTML = calls.join('');
+ decisionCalls.selectedIndex = 0;
+
+ decisionCalls.onchange();
+ };
+
+ detailPanels.show = function(name) {
+ for (p in panels) {
+ if (p == name) {
+ panels[p].style.display = 'block';
+ tabs[p].className = 'selectedtab';
+ }
+ else {
+ panels[p].style.display = 'none';
+ tabs[p].className = '';
+ }
+ }
+ ensureVisible();
+ };
+
+ detailPanels.hide = function() {
+ setPanelWidth(0);
+ }
+
+ decisionCalls.onchange = function() {
+ var val = decisionCalls.value;
+ if (val) {
+ var dec = traceDecision(val.substring(0, val.indexOf('-')));
+ var call = dec.calls[parseInt(val.substring(val.indexOf('-')+1, val.length))];
+
+ if (call.call.indexOf("(default)") !== 0) {
+ callInput.style.color='#000000';
+ callInput.textContent = call.input;
+ if (call.output != null) {
+ callOutput.style.color = '#000000';
+ callOutput.textContent = call.output;
+ } else {
+ if(call.exception !== null){
+ callOutput.style.color = '#ff0000';
+ callOutput.textContent = 'Exception raised!\n\n' + call.exception['class'] + ': ' +
+ call.exception.message + '\n ' + call.exception.backtrace.split('\n').join('\n ');
+ }
+ }
+ } else {
+ callInput.style.color='#999999';
+ callInput.textContent = call.call.replace('(default)', '') + " was not overridden by the resource";
+ callOutput.textContent = '';
+ }
+ } else {
+ callInput.textContent = '';
+ callOutput.textContent = '';
+ }
+ };
+
+ var headersList = function(headers) {
+ var h = '';
+ for (n in headers) h += '<tr><th><code>'+n+':</code></th><td><code>'+headers[n] + '</code></td></tr>';
+ return h;
+ };
+
+ document.getElementById('requestmethod').innerHTML = request.method;
+ document.getElementById('requestpath').innerHTML = request.path;
+ document.getElementById('requestheaders').innerHTML = headersList(request.headers);
+ document.getElementById('requestbody').textContent = request.body;
+
+ document.getElementById('responsecode').innerHTML = response.code;
+ document.getElementById('responseheaders').innerHTML = headersList(response.headers);
+ document.getElementById('responsebody').textContent = response.body;
+
+
+ var infoControls = document.getElementById('infocontrols');
+ var md = false;
+ var dragged = false;
+ var msoff = 0;
+ infoControls.onmousedown = function(ev) {
+ md = true;
+ dragged = false;
+ msoff = ev.clientX-infoPanel.offsetLeft;
+ };
+
+ infoControls.onclick = function(ev) {
+ if (dragged) {
+ lastUsedPanelWidth = panelWidth;
+ }
+ else if (panelWidth < 10) {
+ switch(ev.target.id) {
+ case 'requesttab': detailPanels.show('request'); break;
+ case 'responsetab': detailPanels.show('response'); break;
+ case 'decisiontab': detailPanels.show('decision'); break;
+ default: ensureVisible();
+ }
+ } else {
+ var name = 'none';
+ switch(ev.target.id) {
+ case 'requesttab': name = 'request'; break;
+ case 'responsetab': name = 'response'; break;
+ case 'decisiontab': name = 'decision'; break;
+ }
+
+ if (panels[name] && panels[name].style.display != 'block')
+ detailPanels.show(name);
+ else
+ detailPanels.hide();
+ }
+
+ return false;
+ };
+
+ document.onmousemove = function(ev) {
+ if (md) {
+ dragged = true;
+ panelWidth = windowWidth-(ev.clientX-msoff);
+ if (panelWidth < 0) {
+ panelWidth = 0;
+ infoPanel.style.left = windowWidth+"px";
+ }
+ else if (panelWidth > windowWidth-21) {
+ panelWidth = windowWidth-21;
+ infoPanel.style.left = '21px';
+ }
+ else
+ infoPanel.style.left = (ev.clientX-msoff)+"px";
+
+ canvas.style.marginRight = panelWidth+20+"px";
+ return false;
+ }
+ };
+
+ document.onmouseup = function() { md = false; };
+
+ window.onresize = function() {
+ windowWidth = document.getElementById('sizetest').clientWidth;
+ infoPanel.style.left = windowWidth-panelWidth+'px';
+ };
+
+ detailPanels.setDecision(trace[0]);
+};
+
+window.onload = function() {
+ canvas = document.getElementById('v3map');
+
+ initDetailPanels();
+
+ var scale = 0.25;
+ var coy = canvas.offsetTop;
+ function findDecision(ev) {
+ var x = (ev.clientX+window.pageXOffset)/scale;
+ var y = (ev.clientY+window.pageYOffset-coy)/scale;
+
+ for (var i = trace.length-1; i >= 0; i--) {
+ if (x >= trace[i].x-19 && x <= trace[i].x+19 &&
+ y >= trace[i].y-19 && y <= trace[i].y+19)
+ return trace[i];
+ }
+ };
+
+ var preview = document.getElementById('preview');
+ var previewId = document.getElementById('previewid');
+ var previewCalls = document.getElementById('previewcalls');
+ function previewDecision(dec) {
+ preview.style.left = (dec.x*scale)+'px';
+ preview.style.top = (dec.y*scale+coy+15)+'px';
+ preview.style.display = 'block';
+ previewId.textContent = dec.d;
+
+ previewCalls.innerHTML = dec.previewCalls;
+ };
+
+ function overResponse(ev) {
+ var x = (ev.clientX+window.pageXOffset)/scale;
+ var y = (ev.clientY+window.pageYOffset-coy)/scale;
+
+ return (x >= response.x-(response.width/2)
+ && x <= response.x+(response.width/2)
+ && y >= response.y-19 && y <= response.y+19);
+ };
+
+ decorateTrace();
+
+ var bg = new Image(3138, 2184);
+
+ function drawMap() {
+ var ctx = canvas.getContext("2d");
+
+ ctx.save();
+ ctx.scale(1/scale, 1/scale);
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, 3138, 2184);
+ ctx.restore();
+
+ ctx.drawImage(bg, 0, 0);
+ drawTrace();
+ };
+
+ bg.onload = function() {
+ canvas.getContext("2d").scale(scale, scale);
+
+ document.getElementById('zoomin').onclick = function() {
+ scale = scale*2;
+ canvas.getContext("2d").scale(2, 2);
+ drawMap();
+ };
+
+ document.getElementById('zoomout').onclick = function() {
+ scale = scale/2;
+ canvas.getContext("2d").scale(0.5, 0.5);
+ drawMap();
+ };
+
+ drawMap(scale);
+
+ canvas.onmousemove = function(ev) {
+ if (findDecision(ev)) {
+ canvas.style.cursor = 'pointer';
+ previewDecision(findDecision(ev));
+ }
+ else {
+ preview.style.display = 'none';
+ if (overResponse(ev))
+ canvas.style.cursor = 'pointer';
+ else
+ canvas.style.cursor = 'default';
+ }
+ };
+
+ canvas.onclick = function(ev) {
+ var dec = findDecision(ev);
+ if (dec) {
+ detailPanels.setDecision(dec);
+ detailPanels.show('decision');
+ } else if (overResponse(ev)) {
+ detailPanels.show('response');
+ }
+ };
+ };
+
+ bg.onerror = function() {
+ alert('Failed to load background image.');
+ };
+
+ bg.src = 'static/map.png';
+};
View
129 lib/webmachine/trace/trace_resource.rb
@@ -0,0 +1,129 @@
+require 'erb'
+require 'multi_json'
+
+module Webmachine
+ module Trace
+ # Implements the user-interface of the visual debugger. This
+ # includes serving the static files (the PNG flow diagram, CSS and
+ # JS for the UI) and the HTML for the individual traces.
+ class TraceResource < Resource
+
+ MAP_EXTERNAL = %w{static map.png}
+ MAP_FILE = File.expand_path("../static/http-headers-status-v3.png", __FILE__)
+ SCRIPT_EXTERNAL = %w{static wmtrace.js}
+ SCRIPT_FILE = File.expand_path("../#{SCRIPT_EXTERNAL.join '/'}", __FILE__)
+ STYLE_EXTERNAL = %w{static wmtrace.css}
+ STYLE_FILE = File.expand_path("../#{STYLE_EXTERNAL.join '/'}", __FILE__)
+ TRACELIST_ERB = File.expand_path("../static/tracelist.erb", __FILE__)
+ TRACE_ERB = File.expand_path("../static/trace.erb", __FILE__)
+
+ # The ERB template for the trace list
+ def self.tracelist
+ @@tracelist ||= ERB.new(File.read(TRACELIST_ERB))
+ end
+
+ # The ERB template for a single trace
+ def self.trace
+ @@trace ||= ERB.new(File.read(TRACE_ERB))
+ end
+
+ def content_types_provided
+ case request.path_tokens
+ when []
+ [["text/html", :produce_list]]
+ when MAP_EXTERNAL
+ [["image/png", :produce_file]]
+ when SCRIPT_EXTERNAL
+ [["text/javascript", :produce_file]]
+ when STYLE_EXTERNAL
+ [["text/css", :produce_file]]
+ else
+ [["text/html", :produce_trace]]
+ end
+ end
+
+ def resource_exists?
+ case request.path_tokens
+ when []
+ true
+ when MAP_EXTERNAL
+ @file = MAP_FILE
+ File.exist?(MAP_FILE)
+ when SCRIPT_EXTERNAL
+ @file = SCRIPT_FILE
+ File.exist?(SCRIPT_FILE)
+ when STYLE_EXTERNAL
+ @file = STYLE_FILE
+ File.exist?(STYLE_FILE)
+ else
+ @trace = request.path_tokens.first
+ Trace.traces.include? @trace
+ end
+ end
+
+ def last_modified
+ File.mtime(@file) if @file
+ end
+
+ def expires
+ (Time.now + 30 * 86400).utc if @file
+ end
+
+ def produce_file
+ # TODO: Add support for IO objects as response bodies,
+ # allowing server optimizations like sendfile or chunked
+ # downloads
+ File.read(@file)
+ end
+
+ def produce_list
+ traces = Trace.traces
+ self.class.tracelist.result(binding)
+ end
+
+ def produce_trace
+ data = Trace.fetch(@trace)
+ treq, tres, trace = encode_trace(data)
+ name = @trace
+ self.class.trace.result(binding)
+ end
+
+ def encode_trace(data)
+ data = data.dup
+ # Request is first, response is last
+ treq = data.shift.dup
+ tres = data.pop.dup
+ treq.delete :type
+ tres.delete :type
+ [ MultiJson.dump(treq), MultiJson.dump(tres), MultiJson.dump(encode_decisions(data)) ]
+ end
+
+ def encode_decisions(decisions)
+ decisions.inject([]) do |list, event|
+ case event[:type]
+ when :decision
+ # Don't produce new decisions for sub-steps in the graph
+ unless event[:decision].to_s =~ /[a-z]$/
+ list << {'d' => event[:decision], 'calls' => []}
+ end
+ when :attempt
+ list.last['calls'] << {
+ "call" => event[:name],
+ "source" => event[:source],
+ "input" => event[:args] && event[:args].inspect
+ }
+ when :result
+ list.last['calls'].last['output'] = event[:value].inspect
+ when :exception
+ list.last['calls'].last['exception'] = {
+ 'class' => event[:class],
+ 'backtrace' => event[:backtrace].join("\n"),
+ 'message' => event[:message]
+ }
+ end
+ list
+ end
+ end
+ end
+ end
+end
View
2  lib/webmachine/version.rb
@@ -1,6 +1,6 @@
module Webmachine
# Library version
- VERSION = "0.4.2"
+ VERSION = "1.0.0"
# String for use in "Server" HTTP response header, which includes
# the {VERSION}.
View
19 spec/spec_helper.rb
@@ -16,3 +16,22 @@
config.order = :random
end
end
+
+# For use in specs that need a fully initialized resource
+shared_context "default resource" do
+ let(:method) { 'GET' }
+ let(:uri) { URI.parse("http://localhost/") }
+ let(:headers) { Webmachine::Headers.new }
+ let(:body) { "" }
+ let(:request) { Webmachine::Request.new(method, uri, headers, body) }
+ let(:response) { Webmachine::Response.new }
+
+ let(:resource_class) do
+ Class.new(Webmachine::Resource) do
+ def to_html
+ "<html><body>Hello, world!</body></html>"
+ end
+ end
+ end
+ let(:resource) { resource_class.new(request, response) }
+end
View
113 spec/webmachine/adapters/rack_spec.rb
@@ -1,51 +1,57 @@
require 'spec_helper'
require 'webmachine/adapters/rack'
require 'rack'
+require 'rack/test'
module Test
class Resource < Webmachine::Resource
def allowed_methods
- ["GET", "PUT"]
+ ["GET", "PUT", "POST"]
end
def content_types_accepted
[["application/json", :from_json]]
end
+ def content_types_provided
+ [
+ ["text/html", :to_html],
+ ["application/vnd.webmachine.streaming+enum", :to_enum_stream],
+ ["application/vnd.webmachine.streaming+proc", :to_proc_stream]
+ ]
+ end
+
+ def process_post
+ true
+ end
+
def to_html
response.set_cookie('cookie', 'monster')
response.set_cookie('rodeo', 'clown')
"<html><body>#{request.cookies['string'] || 'testing'}</body></html>"
end
+ def to_enum_stream
+ %w{Hello, World!}
+ end
+
+ def to_proc_stream
+ Proc.new { "Stream" }
+ end
+
def from_json; end
end
end
describe Webmachine::Adapters::Rack do
- let(:env) do
- { "REQUEST_METHOD" => "GET",
- "SCRIPT_NAME" => "",
- "PATH_INFO" => "/test",
- "QUERY_STRING" => "",
- "SERVER_NAME" => "test.server",
- "SERVER_PORT" => 8080,
- "rack.version" => Rack::VERSION,
- "rack.url_scheme" => "http",
- "rack.input" => StringIO.new("Hello, World!"),
- "rack.errors" => StringIO.new,
- "rack.multithread" => false,
- "rack.multiprocess" => true,
- "rack.run_once" => false }
- end
+ include Rack::Test::Methods
let(:configuration) { Webmachine::Configuration.new('0.0.0.0', 8080, :Rack, {}) }
let(:dispatcher) { Webmachine::Dispatcher.new }
let(:adapter) do
described_class.new(configuration, dispatcher)
end
-
- subject { adapter }
+ let(:app) { adapter }
before do
dispatcher.add_route ['test'], Test::Resource
@@ -73,17 +79,17 @@ def from_json; end
end