webmachine 0.4.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/README.md +70 -7
  2. data/Rakefile +19 -0
  3. data/examples/debugger.rb +32 -0
  4. data/examples/webrick.rb +6 -2
  5. data/lib/webmachine.rb +2 -0
  6. data/lib/webmachine/adapters/rack.rb +16 -3
  7. data/lib/webmachine/adapters/webrick.rb +7 -1
  8. data/lib/webmachine/application.rb +10 -10
  9. data/lib/webmachine/cookie.rb +168 -0
  10. data/lib/webmachine/decision/conneg.rb +1 -1
  11. data/lib/webmachine/decision/flow.rb +1 -1
  12. data/lib/webmachine/decision/fsm.rb +19 -12
  13. data/lib/webmachine/decision/helpers.rb +25 -1
  14. data/lib/webmachine/dispatcher.rb +34 -5
  15. data/lib/webmachine/dispatcher/route.rb +2 -0
  16. data/lib/webmachine/media_type.rb +3 -3
  17. data/lib/webmachine/request.rb +11 -0
  18. data/lib/webmachine/resource.rb +3 -1
  19. data/lib/webmachine/resource/authentication.rb +1 -1
  20. data/lib/webmachine/resource/callbacks.rb +16 -0
  21. data/lib/webmachine/resource/tracing.rb +20 -0
  22. data/lib/webmachine/response.rb +38 -8
  23. data/lib/webmachine/trace.rb +74 -0
  24. data/lib/webmachine/trace/fsm.rb +60 -0
  25. data/lib/webmachine/trace/pstore_trace_store.rb +39 -0
  26. data/lib/webmachine/trace/resource_proxy.rb +107 -0
  27. data/lib/webmachine/trace/static/http-headers-status-v3.png +0 -0
  28. data/lib/webmachine/trace/static/trace.erb +54 -0
  29. data/lib/webmachine/trace/static/tracelist.erb +14 -0
  30. data/lib/webmachine/trace/static/wmtrace.css +123 -0
  31. data/lib/webmachine/trace/static/wmtrace.js +725 -0
  32. data/lib/webmachine/trace/trace_resource.rb +129 -0
  33. data/lib/webmachine/version.rb +1 -1
  34. data/spec/spec_helper.rb +19 -0
  35. data/spec/webmachine/adapters/rack_spec.rb +77 -41
  36. data/spec/webmachine/configuration_spec.rb +1 -1
  37. data/spec/webmachine/cookie_spec.rb +99 -0
  38. data/spec/webmachine/decision/conneg_spec.rb +9 -8
  39. data/spec/webmachine/decision/flow_spec.rb +52 -4
  40. data/spec/webmachine/decision/helpers_spec.rb +36 -6
  41. data/spec/webmachine/dispatcher_spec.rb +1 -1
  42. data/spec/webmachine/headers_spec.rb +1 -1
  43. data/spec/webmachine/media_type_spec.rb +1 -1
  44. data/spec/webmachine/request_spec.rb +10 -0
  45. data/spec/webmachine/resource/authentication_spec.rb +3 -3
  46. data/spec/webmachine/response_spec.rb +45 -0
  47. data/spec/webmachine/trace/fsm_spec.rb +32 -0
  48. data/spec/webmachine/trace/resource_proxy_spec.rb +36 -0
  49. data/spec/webmachine/trace/trace_store_spec.rb +29 -0
  50. data/spec/webmachine/trace_spec.rb +17 -0
  51. data/webmachine.gemspec +2 -0
  52. metadata +130 -15
data/README.md CHANGED
@@ -28,7 +28,7 @@ application for it!
28
28
  ```ruby
29
29
  require 'webmachine'
30
30
  # Require any of the files that contain your resources here
31
- require 'my_resource'
31
+ require 'my_resource'
32
32
 
33
33
  # Create an application which encompasses routes and configruation
34
34
  MyApp = Webmachine::Application.new do |app|
@@ -63,7 +63,7 @@ class MyResource < Webmachine::Resource
63
63
  def encodings_provided
64
64
  {"gzip" => :encode_gzip, "identity" => :encode_identity}
65
65
  end
66
-
66
+
67
67
  def to_html
68
68
  "<html><body>Hello, world!</body></html>"
69
69
  end
@@ -85,7 +85,7 @@ object, `Webmachine.application` will return a global one.
85
85
  ```ruby
86
86
  require 'webmachine'
87
87
  require 'my_resource'
88
-
88
+
89
89
  Webmachine.application.routes do
90
90
  add ['*'], MyResource
91
91
  end
@@ -95,11 +95,52 @@ Webmachine.application.configure do |config|
95
95
  config.port = 3000
96
96
  config.adapter = :Mongrel
97
97
  end
98
-
98
+
99
99
  # Start the server.
100
100
  Webmachine.application.run
101
101
  ```
102
102
 
103
+ ### Visual debugger
104
+
105
+ It can be hard to understand all of the decisions that Webmachine
106
+ makes when servicing a request to your resource, which is why we have
107
+ the "visual debugger". In development, you can turn on tracing of the
108
+ decision graph for a resource by implementing the `#trace?` callback
109
+ so that it returns true:
110
+
111
+ ```ruby
112
+ class MyTracedResource < Webmachine::Resource
113
+ def trace?
114
+ true
115
+ end
116
+
117
+ # The rest of your callbacks...
118
+ end
119
+ ```
120
+
121
+ Then enable the visual debugger resource by adding a route to your
122
+ configuration:
123
+
124
+ ```ruby
125
+ Webmachine.application.routes do
126
+ # This can be any path as long as it ends with '*'
127
+ add ['trace', '*'], Webmachine::Trace::TraceResource
128
+ # The rest of your routes...
129
+ end
130
+ ```
131
+
132
+ Now when you visit your traced resource, a trace of the request
133
+ process will be recorded in memory. Open your browser to `/trace` to
134
+ list the recorded traces and inspect the result. The response from your
135
+ traced resource will also include the `X-Webmachine-Trace-Id` that you
136
+ can use to lookup the trace. It might look something like this:
137
+
138
+ ![preview calls at decision](http://seancribbs-skitch.s3.amazonaws.com/Webmachine_Trace_2156885920-20120625-100153.png)
139
+
140
+ Refer to
141
+ [examples/debugger.rb](/seancribbs/webmachine-ruby/blob/master/examples/debugger.rb)
142
+ for an example of how to enable the debugger.
143
+
103
144
  ## Features
104
145
 
105
146
  * Handles the hard parts of content negotiation, conditional
@@ -112,13 +153,12 @@ Webmachine.application.run
112
153
  * Streaming/chunked response bodies are permitted as Enumerables,
113
154
  Procs, or Fibers!
114
155
  * Unlike the Erlang original, it does real Language negotiation.
156
+ * Includes the visual debugger so you can look through the decision
157
+ graph to determine how your resources are behaving.
115
158
 
116
159
  ## Problems/TODOs
117
160
 
118
161
  * Command-line tools, and general polish.
119
- * Tracing is exposed as an Array of decisions visited on the response
120
- object. You should be able to turn this off and on, and visualize
121
- the decisions on the sequence diagram.
122
162
 
123
163
  ## LICENSE
124
164
 
@@ -128,6 +168,29 @@ LICENSE for details.
128
168
 
129
169
  ## Changelog
130
170
 
171
+ ### 1.0.0 July 7, 2012
172
+
173
+ 1.0.0 is a major feature release that finally includes the visual
174
+ debugger, some nice cookie support, and some new extension
175
+ points. Added Peter Johanson and Armin Joellenbeck as
176
+ contributors. Thank you for your contributions!
177
+
178
+ * A cookie parsing and manipulation API was added.
179
+ * Conneg headers now accept any amount of whitespace around commas,
180
+ including none.
181
+ * `Callbacks#handle_exception` was added so that resources can handle
182
+ exceptions that they generate and produce more friendly responses.
183
+ * Chunked and non-chunked response bodies in the Rack adapter were
184
+ fixed.
185
+ * The WEBrick example was updated to use the new API.
186
+ * `Dispatcher` was refactored so that you can modify how resources
187
+ are initialized before dispatching occurs.
188
+ * `Route` now includes the `Translation` module so that exception
189
+ messages are properly rendered.
190
+ * The visual debugger was added (more details in the README).
191
+ * The `Content-Length` header will always be set inside Webmachine and
192
+ is no longer reliant on the adapter to set it.
193
+
131
194
  ### 0.4.2 March 22, 2012
132
195
 
133
196
  0.4.2 is a bugfix release that corrects a few minor issues. Added Lars
data/Rakefile CHANGED
@@ -32,6 +32,25 @@ task :release => :gem do
32
32
  system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
33
33
  end
34
34
 
35
+ desc "Cleans up white space in source files"
36
+ task :clean_whitespace do
37
+ no_file_cleaned = true
38
+
39
+ Dir["**/*.rb"].each do |file|
40
+ contents = File.read(file)
41
+ cleaned_contents = contents.gsub(/([ \t]+)$/, '')
42
+ unless cleaned_contents == contents
43
+ no_file_cleaned = false
44
+ puts " - Cleaned #{file}"
45
+ File.open(file, 'w') { |f| f.write(cleaned_contents) }
46
+ end
47
+ end
48
+
49
+ if no_file_cleaned
50
+ puts "No files with trailing whitespace found"
51
+ end
52
+ end
53
+
35
54
  require 'rspec/core'
36
55
  require 'rspec/core/rake_task'
37
56
 
@@ -0,0 +1,32 @@
1
+ require 'webmachine'
2
+ require 'webmachine/trace'
3
+
4
+ class MyTracedResource < Webmachine::Resource
5
+ def trace?; true; end
6
+
7
+ def resource_exists?
8
+ case request.query['e']
9
+ when 'true'
10
+ true
11
+ when 'fail'
12
+ raise "BOOM"
13
+ else
14
+ false
15
+ end
16
+ end
17
+
18
+ def to_html
19
+ "<html>You found me.</html>"
20
+ end
21
+ end
22
+
23
+ # Webmachine::Trace.trace_store = :pstore, "./trace"
24
+
25
+ TraceExample = Webmachine::Application.new do |app|
26
+ app.routes do
27
+ add ['trace', '*'], Webmachine::Trace::TraceResource
28
+ add [], MyTracedResource
29
+ end
30
+ end
31
+
32
+ TraceExample.run
@@ -14,6 +14,10 @@ class HelloResource < Webmachine::Resource
14
14
  end
15
15
  end
16
16
 
17
- Webmachine::Dispatcher.add_route([], HelloResource)
17
+ App = Webmachine::Application.new do |app|
18
+ app.routes do
19
+ add [], HelloResource
20
+ end
21
+ end
18
22
 
19
- Webmachine.run
23
+ App.run
@@ -1,4 +1,5 @@
1
1
  require 'webmachine/configuration'
2
+ require 'webmachine/cookie'
2
3
  require 'webmachine/headers'
3
4
  require 'webmachine/request'
4
5
  require 'webmachine/response'
@@ -10,6 +11,7 @@ require 'webmachine/adapters'
10
11
  require 'webmachine/dispatcher'
11
12
  require 'webmachine/application'
12
13
  require 'webmachine/resource'
14
+ require 'webmachine/trace'
13
15
  require 'webmachine/version'
14
16
 
15
17
  # Webmachine is a toolkit for making well-behaved HTTP applications.
@@ -4,6 +4,7 @@ require 'webmachine/headers'
4
4
  require 'webmachine/request'
5
5
  require 'webmachine/response'
6
6
  require 'webmachine/dispatcher'
7
+ require 'webmachine/chunked_body'
7
8
 
8
9
  module Webmachine
9
10
  module Adapters
@@ -58,10 +59,22 @@ module Webmachine
58
59
 
59
60
  response.headers['Server'] = [Webmachine::SERVER_STRING, "Rack/#{::Rack.version}"].join(" ")
60
61
 
61
- body = response.body.respond_to?(:call) ? response.body.call : response.body
62
- body = body.is_a?(String) ? [ body ] : body
62
+ rack_status = response.code
63
+ rack_headers = response.headers.flattened("\n")
64
+ rack_body = case response.body
65
+ when String # Strings are enumerable in ruby 1.8
66
+ [response.body]
67
+ else
68
+ if response.body.respond_to?(:call)
69
+ Webmachine::ChunkedBody.new(Array(response.body.call))
70
+ elsif response.body.respond_to?(:each)
71
+ Webmachine::ChunkedBody.new(response.body)
72
+ else
73
+ [response.body.to_s]
74
+ end
75
+ end
63
76
 
64
- [response.code.to_i, response.headers, body || []]
77
+ [rack_status, rack_headers, rack_body]
65
78
  end
66
79
 
67
80
  # Wraps the Rack input so it can be treated like a String or
@@ -39,7 +39,13 @@ module Webmachine
39
39
  response = Webmachine::Response.new
40
40
  @dispatcher.dispatch(request, response)
41
41
  wres.status = response.code.to_i
42
- response.headers.each { |k,v| wres[k] = v }
42
+
43
+ headers = response.headers.flattened.reject { |k,v| k == 'Set-Cookie' }
44
+ headers.each { |k,v| wres[k] = v }
45
+
46
+ cookies = [response.headers['Set-Cookie'] || []].flatten
47
+ cookies.each { |c| wres.cookies << c }
48
+
43
49
  wres['Server'] = [Webmachine::SERVER_STRING, wres.config[:ServerSoftware]].join(" ")
44
50
  case response.body
45
51
  when String
@@ -4,19 +4,19 @@ require 'webmachine/dispatcher'
4
4
 
5
5
  module Webmachine
6
6
  # How to get your Webmachine app running:
7
- #
7
+ #
8
8
  # MyApp = Webmachine::Application.new do |app|
9
9
  # app.routes do
10
10
  # add ['*'], AssetResource
11
11
  # end
12
- #
12
+ #
13
13
  # app.configure do |config|
14
14
  # config.port = 8888
15
15
  # end
16
16
  # end
17
- #
17
+ #
18
18
  # MyApp.run
19
- #
19
+ #
20
20
  class Application
21
21
  extend Forwardable
22
22
 
@@ -32,17 +32,17 @@ module Webmachine
32
32
  #
33
33
  # An instance of application contains Adapter configuration and
34
34
  # a Dispatcher instance which can be configured with Routes.
35
- #
35
+ #
36
36
  # @param [Webmachine::Configuration] configuration
37
37
  # a Webmachine::Configuration
38
- #
38
+ #
39
39
  # @yield [app]
40
40
  # a block in which to configure this Application
41
41
  # @yieldparam [Application]
42
42
  # the Application instance being initialized
43
- def initialize(configuration = Configuration.default)
43
+ def initialize(configuration = Configuration.default, dispatcher = Dispatcher.new)
44
44
  @configuration = configuration
45
- @dispatcher = Dispatcher.new
45
+ @dispatcher = dispatcher
46
46
 
47
47
  yield self if block_given?
48
48
  end
@@ -66,10 +66,10 @@ module Webmachine
66
66
 
67
67
  # Evaluates the passed block in the context of {Webmachine::Dispatcher}
68
68
  # for use in adding a number of routes at once.
69
- #
69
+ #
70
70
  # @return [Application, Array<Route>]
71
71
  # self if configuring, or an Array of Routes otherwise
72
- #
72
+ #
73
73
  # @see Webmachine::Dispatcher#add_route
74
74
  def routes(&block)
75
75
  if block_given?
@@ -0,0 +1,168 @@
1
+ require 'uri'
2
+
3
+ module Webmachine
4
+ # An HTTP Cookie for a response, including optional attributes
5
+ class Cookie
6
+ # Parse a Cookie header, with any number of cookies, into a hash
7
+ # @param [String] the Cookie header
8
+ # @param [Boolean] whether to include duplicate cookie values in the
9
+ # response
10
+ # @return [Hash] cookie name/value pairs.
11
+ def self.parse(cstr, include_dups = false)
12
+ cookies = {}
13
+ (cstr || '').split(/\s*[;,]\s*/n).each { |c|
14
+ k,v = c.split(/\s*=\s*/, 2).map { |s| unescape(s) }
15
+
16
+ case cookies[k]
17
+ when nil
18
+ cookies[k] = v
19
+ when Array
20
+ cookies[k] << v
21
+ else
22
+ cookies[k] = [cookies[k], v] if include_dups
23
+ end
24
+ }
25
+
26
+ cookies
27
+ end
28
+
29
+ attr_reader :name, :value
30
+
31
+ # Allowed keys for the attributes parameter of
32
+ # {Webmachine::Cookie#initialize}
33
+ ALLOWED_ATTRIBUTES = [:secure, :httponly, :path, :domain,
34
+ :comment, :maxage, :expires, :version]
35
+
36
+ # If the cookie is HTTP only
37
+ def http_only?
38
+ @attributes[:httponly]
39
+ end
40
+
41
+ # If the cookie should be treated as a secure one by the client
42
+ def secure?
43
+ @attributes[:secure]
44
+ end
45
+
46
+ # The path for which the cookie is valid
47
+ def path
48
+ @attributes[:path]
49
+ end
50
+
51
+ # The domain for which the cookie is valid
52
+ def domain
53
+ @attributes[:domain]
54
+ end
55
+
56
+ # A comment allowing documentation on the intended use for the cookie
57
+ def comment
58
+ @attributes[:comment]
59
+ end
60
+
61
+ # Which version of the state management specification the cookie conforms
62
+ def version
63
+ @attributes[:version]
64
+ end
65
+
66
+ # The Max-Age, in seconds, for which the cookie is valid
67
+ def maxage
68
+ @attributes[:maxage]
69
+ end
70
+
71
+ # The expiration {DateTime} of the cookie
72
+ def expires
73
+ @attributes[:expires]
74
+ end
75
+
76
+ def initialize(name, value, attributes = {})
77
+ @name, @value, @attributes = name, value, attributes
78
+ end
79
+
80
+ # Convert to an RFC2109 valid cookie string
81
+ # @return [String] The RFC2109 valid cookie string
82
+ def to_s
83
+ attributes = ALLOWED_ATTRIBUTES.select { |a| @attributes[a] }.map do |a|
84
+ case a
85
+ when :httponly
86
+ "HttpOnly" if @attributes[a]
87
+ when :secure
88
+ "Secure" if @attributes[a]
89
+ when :maxage
90
+ "MaxAge=" + @attributes[a].to_s
91
+ when :expires
92
+ "Expires=" + rfc2822(@attributes[a])
93
+ when :comment
94
+ "Comment=" + escape(@attributes[a].to_s)
95
+ else
96
+ a.to_s.sub(/^\w/) { $&.capitalize } + "=" + @attributes[a].to_s
97
+ end
98
+ end
99
+
100
+ ([escape(name) + "=" + escape(value)] + attributes).join("; ")
101
+ end
102
+
103
+ private
104
+
105
+ def rfc2822(time)
106
+ wday = Time::RFC2822_DAY_NAME[time.wday]
107
+ mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
108
+ time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
109
+ end
110
+
111
+ if URI.respond_to?(:decode_www_form_component) and defined?(::Encoding)
112
+ # Escape a cookie
113
+ def escape(s)
114
+ URI.encode_www_form_component(s)
115
+ end
116
+
117
+ # Unescape a cookie
118
+ # @private
119
+ def self.unescape(s, encoding = Encoding::UTF_8)
120
+ URI.decode_www_form_component(s, encoding)
121
+ end
122
+ else # We're on 1.8.7, or JRuby or Rubinius in 1.8 mode
123
+ # Copied and modified from 1.9.x URI
124
+ # @private
125
+ TBLENCWWWCOMP_ = {}
126
+ 256.times do |i|
127
+ TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
128
+ end
129
+ TBLENCWWWCOMP_[' '] = '+'
130
+ TBLENCWWWCOMP_.freeze
131
+
132
+ # @private
133
+ TBLDECWWWCOMP_ = {}
134
+ 256.times do |i|
135
+ h, l = i>>4, i&15
136
+ TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
137
+ TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
138
+ TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
139
+ TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
140
+ end
141
+ TBLDECWWWCOMP_['+'] = ' '
142
+ TBLDECWWWCOMP_.freeze
143
+
144
+ # Decode given +str+ of URL-encoded form data.
145
+ #
146
+ # This decodes + to SP.
147
+ #
148
+ # @private
149
+ def self.unescape(str, enc=nil)
150
+ raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%\h\h|[^%]+)*\z/ =~ str
151
+ str.gsub(/\+|%\h\h/){|c| TBLDECWWWCOMP_[c] }
152
+ end
153
+
154
+ # Encode given +str+ to URL-encoded form data.
155
+ #
156
+ # This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP
157
+ # (ASCII space) to + and converts others to %XX.
158
+ #
159
+ # This is an implementation of
160
+ # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
161
+ #
162
+ # @private
163
+ def escape(str)
164
+ str.to_s.gsub(/[^*\-.0-9A-Z_a-z]/){|c| TBLENCWWWCOMP_[c] }
165
+ end
166
+ end
167
+ end
168
+ end