webmachine 0.4.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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