webmachine 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/Gemfile +5 -4
  2. data/README.md +25 -2
  3. data/examples/debugger.rb +7 -0
  4. data/examples/logging.rb +41 -0
  5. data/lib/webmachine/adapters.rb +1 -1
  6. data/lib/webmachine/adapters/hatetepe.rb +5 -1
  7. data/lib/webmachine/adapters/lazy_request_body.rb +7 -7
  8. data/lib/webmachine/adapters/mongrel.rb +27 -12
  9. data/lib/webmachine/adapters/rack.rb +42 -6
  10. data/lib/webmachine/adapters/reel.rb +91 -23
  11. data/lib/webmachine/adapters/webrick.rb +12 -6
  12. data/lib/webmachine/application.rb +1 -0
  13. data/lib/webmachine/cookie.rb +1 -1
  14. data/lib/webmachine/dispatcher.rb +7 -1
  15. data/lib/webmachine/events.rb +179 -0
  16. data/lib/webmachine/events/instrumented_event.rb +19 -0
  17. data/lib/webmachine/response.rb +1 -1
  18. data/lib/webmachine/streaming/io_encoder.rb +5 -1
  19. data/lib/webmachine/trace.rb +18 -0
  20. data/lib/webmachine/trace/fsm.rb +4 -1
  21. data/lib/webmachine/trace/listener.rb +12 -0
  22. data/lib/webmachine/version.rb +1 -1
  23. data/spec/spec_helper.rb +11 -0
  24. data/spec/support/adapter_lint.rb +125 -0
  25. data/spec/support/test_resource.rb +73 -0
  26. data/spec/webmachine/adapters/hatetepe_spec.rb +2 -2
  27. data/spec/webmachine/adapters/mongrel_spec.rb +6 -51
  28. data/spec/webmachine/adapters/rack_spec.rb +22 -155
  29. data/spec/webmachine/adapters/reel_spec.rb +59 -7
  30. data/spec/webmachine/adapters/webrick_spec.rb +7 -13
  31. data/spec/webmachine/cookie_spec.rb +1 -1
  32. data/spec/webmachine/decision/helpers_spec.rb +10 -0
  33. data/spec/webmachine/events_spec.rb +58 -0
  34. data/spec/webmachine/request_spec.rb +41 -0
  35. data/webmachine.gemspec +1 -1
  36. metadata +63 -24
@@ -9,16 +9,22 @@ module Webmachine
9
9
  module Adapters
10
10
  # Connects Webmachine to WEBrick.
11
11
  class WEBrick < Adapter
12
+ # Used to override default WEBRick options (useful in testing)
13
+ DEFAULT_OPTIONS = {}
12
14
 
13
15
  # Starts the WEBrick adapter
14
16
  def run
15
- options = {
17
+ options = DEFAULT_OPTIONS.merge({
16
18
  :Port => configuration.port,
17
19
  :BindAddress => configuration.ip
18
- }.merge(configuration.adapter_options)
19
- server = Webmachine::Adapters::WEBrick::Server.new(dispatcher, options)
20
- trap("INT"){ server.shutdown }
21
- Thread.new { server.start }.join
20
+ }).merge(configuration.adapter_options)
21
+ @server = Server.new(dispatcher, options)
22
+ trap("INT") { shutdown }
23
+ @server.start
24
+ end
25
+
26
+ def shutdown
27
+ @server.shutdown if @server
22
28
  end
23
29
 
24
30
  # WEBRick::HTTPServer that is run by the WEBrick adapter.
@@ -51,7 +57,7 @@ module Webmachine
51
57
  when String
52
58
  wres.body << response.body
53
59
  when Enumerable
54
- wres.chunked = true
60
+ wres.chunked = response.headers['Transfer-Encoding'] == 'chunked'
55
61
  response.body.each {|part| wres.body << part }
56
62
  else
57
63
  if response.body.respond_to?(:call)
@@ -1,6 +1,7 @@
1
1
  require 'forwardable'
2
2
  require 'webmachine/configuration'
3
3
  require 'webmachine/dispatcher'
4
+ require 'webmachine/events'
4
5
 
5
6
  module Webmachine
6
7
  # How to get your Webmachine app running:
@@ -87,7 +87,7 @@ module Webmachine
87
87
  when :secure
88
88
  "Secure" if @attributes[a]
89
89
  when :maxage
90
- "MaxAge=" + @attributes[a].to_s
90
+ "Max-Age=" + @attributes[a].to_s
91
91
  when :expires
92
92
  "Expires=" + rfc2822(@attributes[a])
93
93
  when :comment
@@ -40,7 +40,13 @@ module Webmachine
40
40
  # @param [Response] response the response object
41
41
  def dispatch(request, response)
42
42
  if resource = find_resource(request, response)
43
- Webmachine::Decision::FSM.new(resource, request, response).run
43
+ Webmachine::Events.instrument('wm.dispatch') do |payload|
44
+ Webmachine::Decision::FSM.new(resource, request, response).run
45
+
46
+ payload[:resource] = resource.class.name
47
+ payload[:request] = request.dup
48
+ payload[:code] = response.code
49
+ end
44
50
  else
45
51
  Webmachine.render_error(404, request, response)
46
52
  end
@@ -0,0 +1,179 @@
1
+ require 'securerandom' # For AS::Notifications
2
+ require 'as/notifications'
3
+ require 'webmachine/events/instrumented_event'
4
+
5
+ module Webmachine
6
+ # {Webmachine::Events} implements the
7
+ # [ActiveSupport::Notifications](http://rubydoc.info/gems/activesupport/ActiveSupport/Notifications)
8
+ # instrumentation API. It delegates to the configured backend.
9
+ # The default backend is
10
+ # [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications).
11
+ #
12
+ # # Published events
13
+ #
14
+ # Webmachine publishes some internal events by default. All of them use
15
+ # the `wm.` prefix.
16
+ #
17
+ # ## `wm.dispatch` ({.instrument})
18
+ #
19
+ # The payload hash includes the following keys.
20
+ #
21
+ # * `:resource` - The resource class name
22
+ # * `:request` - A copy of the request object
23
+ # * `:code` - The response code
24
+ #
25
+ module Events
26
+ class << self
27
+ # The class that {Webmachine::Events} delegates all messages to.
28
+ # (default [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications))
29
+ #
30
+ # It can be changed to an
31
+ # [ActiveSupport::Notifications](http://rubydoc.info/gems/activesupport/ActiveSupport/Notifications)
32
+ # compatible backend early in the application startup process.
33
+ #
34
+ # @example
35
+ # require 'webmachine'
36
+ # require 'active_support/notifications'
37
+ #
38
+ # Webmachine::Events.backend = ActiveSupport::Notifications
39
+ #
40
+ # Webmachine::Application.new {|app|
41
+ # # setup application
42
+ # }.run
43
+ attr_accessor :backend
44
+
45
+ # Publishes the given arguments to all listeners subscribed to the given
46
+ # event name.
47
+ # @param name [String] the event name
48
+ # @example
49
+ # Webmachine::Events.publish('wm.foo', :hello => 'world')
50
+ def publish(name, *args)
51
+ backend.publish(name, *args)
52
+ end
53
+
54
+ # Instrument the given block by measuring the time taken to execute it
55
+ # and publish it. Notice that events get sent even if an error occurs
56
+ # in the passed-in block.
57
+ #
58
+ # If an exception happens during an instrumentation the payload will
59
+ # have a key `:exception` with an array of two elements as value:
60
+ # a string with the name of the exception class, and the exception
61
+ # message. (when using the default
62
+ # [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications)
63
+ # backend)
64
+ #
65
+ # @param name [String] the event name
66
+ # @param payload [Hash] the initial payload
67
+ #
68
+ # @example
69
+ # Webmachine::Events.instrument('wm.dispatch') do |payload|
70
+ # execute_some_method
71
+ #
72
+ # payload[:custom_payload_value] = 'important'
73
+ # end
74
+ def instrument(name, payload = {}, &block)
75
+ backend.instrument(name, payload, &block)
76
+ end
77
+
78
+ # Subscribes to the given event name.
79
+ #
80
+ # @note The documentation of this method describes its behaviour with the
81
+ # default backed [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications).
82
+ # It can change if a different backend is used.
83
+ #
84
+ # @overload subscribe(name)
85
+ # Subscribing to an {.instrument} event. The block arguments can be
86
+ # passed to {Webmachine::Events::InstrumentedEvent}.
87
+ #
88
+ # @param name [String, Regexp] the event name to subscribe to
89
+ # @yieldparam name [String] the event name
90
+ # @yieldparam start [Time] the event start time
91
+ # @yieldparam end [Time] the event end time
92
+ # @yieldparam event_id [String] the event id
93
+ # @yieldparam payload [Hash] the event payload
94
+ # @return [Object] the subscriber object (type depends on the backend implementation)
95
+ #
96
+ # @example
97
+ # # Subscribe to all 'wm.dispatch' events
98
+ # Webmachine::Events.subscribe('wm.dispatch') {|*args|
99
+ # event = Webmachine::Events::InstrumentedEvent.new(*args)
100
+ # }
101
+ #
102
+ # # Subscribe to all events that start with 'wm.'
103
+ # Webmachine::Events.subscribe(/wm\.*/) {|*args| }
104
+ #
105
+ # @overload subscribe(name)
106
+ # Subscribing to a {.publish} event.
107
+ #
108
+ # @param name [String, Regexp] the event name to subscribe to
109
+ # @yieldparam name [String] the event name
110
+ # @yieldparam *args [Array] the published arguments
111
+ # @return [Object] the subscriber object (type depends on the backend implementation)
112
+ #
113
+ # @example
114
+ # Webmachine::Events.subscribe('custom.event') {|name, *args|
115
+ # args #=> [obj1, obj2, {:num => 1}]
116
+ # }
117
+ #
118
+ # Webmachine::Events.publish('custom.event', obj1, obj2, {:num => 1})
119
+ #
120
+ # @overload subscribe(name, listener)
121
+ # Subscribing with a listener object instead of a block. The listener
122
+ # object must respond to `#call`.
123
+ #
124
+ # @param name [String, Regexp] the event name to subscribe to
125
+ # @param listener [#call] a listener object
126
+ # @return [Object] the subscriber object (type depends on the backend implementation)
127
+ #
128
+ # @example
129
+ # class CustomListener
130
+ # def call(name, *args)
131
+ # # ...
132
+ # end
133
+ # end
134
+ #
135
+ # Webmachine::Events.subscribe('wm.dispatch', CustomListener.new)
136
+ #
137
+ def subscribe(name, *args, &block)
138
+ backend.subscribe(name, *args, &block)
139
+ end
140
+
141
+ # Subscribe to an event temporarily while the block runs.
142
+ #
143
+ # @note The documentation of this method describes its behaviour with the
144
+ # default backed [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications).
145
+ # It can change if a different backend is used.
146
+ #
147
+ # The callback in the following example will be called for all
148
+ # "sql.active_record" events instrumented during the execution of the
149
+ # block. The callback is unsubscribed automatically after that.
150
+ #
151
+ # @example
152
+ # callback = lambda {|name, *args| handle_event(name, *args) }
153
+ #
154
+ # Webmachine::Events.subscribed(callback, 'sql.active_record') do
155
+ # call_active_record
156
+ # end
157
+ def subscribed(callback, name, &block)
158
+ backend.subscribed(callback, name, &block)
159
+ end
160
+
161
+ # Unsubscribes the given subscriber.
162
+ #
163
+ # @note The documentation of this method describes its behaviour with the
164
+ # default backed [AS::Notifications](http://rubydoc.info/gems/as-notifications/AS/Notifications).
165
+ # It can change if a different backend is used.
166
+ #
167
+ # @param subscriber [Object] the subscriber object (type depends on the backend implementation)
168
+ # @example
169
+ # subscriber = Webmachine::Events.subscribe('wm.dispatch') {|*args| }
170
+ #
171
+ # Webmachine::Events.unsubscribe(subscriber)
172
+ def unsubscribe(subscriber)
173
+ backend.unsubscribe(subscriber)
174
+ end
175
+ end
176
+
177
+ self.backend = AS::Notifications
178
+ end
179
+ end
@@ -0,0 +1,19 @@
1
+ require 'delegate'
2
+ require 'as/notifications/instrumenter'
3
+
4
+ module Webmachine
5
+ module Events
6
+ # {Webmachine::Events::InstrumentedEvent} delegates to
7
+ # [AS::Notifications::Event](http://rubydoc.info/gems/as-notifications/AS/Notifications/Event).
8
+ #
9
+ # The class
10
+ # [AS::Notifications::Event](http://rubydoc.info/gems/as-notifications/AS/Notifications/Event)
11
+ # is able to take the arguments of an {Webmachine::Events.instrument} event
12
+ # and provide an object-oriented interface to that data.
13
+ class InstrumentedEvent < SimpleDelegator
14
+ def initialize(*args)
15
+ super(AS::Notifications::Event.new(*args))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -45,7 +45,7 @@ module Webmachine
45
45
  # @param [String] value the value of the cookie
46
46
  # @param [Hash] attributes for the cookie. See RFC2109.
47
47
  def set_cookie(name, value, attributes = {})
48
- cookie = Webmachine::Cookie.new(name, value).to_s
48
+ cookie = Webmachine::Cookie.new(name, value, attributes).to_s
49
49
  case headers['Set-Cookie']
50
50
  when nil
51
51
  headers['Set-Cookie'] = cookie
@@ -13,7 +13,7 @@ module Webmachine
13
13
  # @yield [chunk]
14
14
  # @yieldparam [String] chunk a chunk of the response, encoded
15
15
  def each
16
- while chunk = body.read(CHUNK_SIZE) && chunk != ""
16
+ while chunk = body.read(CHUNK_SIZE) and chunk != ""
17
17
  yield resource.send(encoder, resource.send(charsetter, chunk))
18
18
  end
19
19
  end
@@ -50,6 +50,10 @@ module Webmachine
50
50
  end
51
51
  end
52
52
 
53
+ def empty?
54
+ size == 0
55
+ end
56
+
53
57
  alias bytesize size
54
58
 
55
59
  private
@@ -2,6 +2,7 @@ require 'webmachine/trace/resource_proxy'
2
2
  require 'webmachine/trace/fsm'
3
3
  require 'webmachine/trace/pstore_trace_store'
4
4
  require 'webmachine/trace/trace_resource'
5
+ require 'webmachine/trace/listener'
5
6
 
6
7
  module Webmachine
7
8
  # Contains means to enable the Webmachine visual debugger.
@@ -13,6 +14,11 @@ module Webmachine
13
14
  :pstore => PStoreTraceStore
14
15
  }
15
16
 
17
+ DEFAULT_TRACE_SUBSCRIBER = Webmachine::Events.subscribe(
18
+ /wm\.trace\..+/,
19
+ Webmachine::Trace::Listener.new
20
+ )
21
+
16
22
  # Determines whether this resource instance should be traced.
17
23
  # @param [Webmachine::Resource] resource a resource instance
18
24
  # @return [true, false] whether to trace the resource
@@ -70,5 +76,17 @@ module Webmachine
70
76
  end
71
77
  end
72
78
  private :trace_store
79
+
80
+ # Sets the trace listener objects.
81
+ # Defaults to Webmachine::Trace::Listener.new.
82
+ # @param [Array<Object>] listeners a list of event listeners
83
+ # @return [Array<Object>] a list of event subscribers
84
+ def trace_listener=(listeners)
85
+ Webmachine::Events.unsubscribe(DEFAULT_TRACE_SUBSCRIBER)
86
+
87
+ Array(listeners).map do |listener|
88
+ Webmachine::Events.subscribe(/wm\.trace\..+/, listener)
89
+ end
90
+ end
73
91
  end
74
92
  end
@@ -27,7 +27,10 @@ module Webmachine
27
27
  :body => trace_response_body(response.body)
28
28
  }
29
29
  ensure
30
- Trace.record(resource.object_id.to_s, response.trace)
30
+ Webmachine::Events.publish('wm.trace.record', {
31
+ :trace_id => resource.object_id.to_s,
32
+ :trace => response.trace
33
+ })
31
34
  end
32
35
 
33
36
  # Adds a decision to the trace.
@@ -0,0 +1,12 @@
1
+ module Webmachine
2
+ module Trace
3
+ class Listener
4
+ def call(name, payload)
5
+ key = payload.fetch(:trace_id)
6
+ trace = payload.fetch(:trace)
7
+
8
+ Webmachine::Trace.record(key, trace)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,6 +1,6 @@
1
1
  module Webmachine
2
2
  # Library version
3
- VERSION = "1.1.0"
3
+ VERSION = "1.2.0"
4
4
 
5
5
  # String for use in "Server" HTTP response header, which includes
6
6
  # the {VERSION}.
data/spec/spec_helper.rb CHANGED
@@ -4,17 +4,28 @@ $LOAD_PATH << File.expand_path("../../lib", __FILE__)
4
4
  require 'rubygems'
5
5
  require 'webmachine'
6
6
  require 'rspec'
7
+ require 'logger'
7
8
 
8
9
  RSpec.configure do |config|
9
10
  config.mock_with :rspec
10
11
  config.filter_run :focus => true
11
12
  config.run_all_when_everything_filtered = true
12
13
  config.treat_symbols_as_metadata_keys_with_true_values = true
14
+ config.formatter = :documentation if ENV['CI']
13
15
  if defined?(::Java)
14
16
  config.seed = Time.now.utc
15
17
  else
16
18
  config.order = :random
17
19
  end
20
+
21
+ config.before(:suite) do
22
+ options = {
23
+ :Logger => Logger.new("/dev/null"),
24
+ :AccessLog => []
25
+ }
26
+ Webmachine::Adapters::WEBrick::DEFAULT_OPTIONS.merge! options
27
+ Webmachine::Adapters::Rack::DEFAULT_OPTIONS.merge! options
28
+ end
18
29
  end
19
30
 
20
31
  # For use in specs that need a fully initialized resource
@@ -0,0 +1,125 @@
1
+ require "support/test_resource"
2
+ require "net/http"
3
+
4
+ shared_examples_for :adapter_lint do
5
+ attr_accessor :client
6
+
7
+ before(:all) do
8
+ configuration = Webmachine::Configuration.default
9
+ dispatcher = Webmachine::Dispatcher.new
10
+ dispatcher.add_route ["test"], Test::Resource
11
+
12
+ @adapter = described_class.new(configuration, dispatcher)
13
+ @client = Net::HTTP.new(configuration.ip, configuration.port)
14
+
15
+ Thread.abort_on_exception = true
16
+ @server_thread = Thread.new { @adapter.run }
17
+
18
+ # Wait until the server is responsive
19
+ timeout(5) do
20
+ begin
21
+ client.start
22
+ rescue Errno::ECONNREFUSED
23
+ sleep(0.1)
24
+ retry
25
+ end
26
+ end
27
+ end
28
+
29
+ after(:all) do
30
+ @adapter.shutdown
31
+ @client.finish
32
+ @server_thread.join
33
+ end
34
+
35
+ it "provides a string-like request body" do
36
+ request = Net::HTTP::Put.new("/test")
37
+ request.body = "Hello, World!"
38
+ request["Content-Type"] = "test/request.stringbody"
39
+ response = client.request(request)
40
+ response["Content-Length"].should eq("21")
41
+ response.body.should eq("String: Hello, World!")
42
+ end
43
+
44
+ it "provides an enumerable request body" do
45
+ request = Net::HTTP::Put.new("/test")
46
+ request.body = "Hello, World!"
47
+ request["Content-Type"] = "test/request.enumbody"
48
+ response = client.request(request)
49
+ response["Content-Length"].should eq("19")
50
+ response.body.should eq("Enum: Hello, World!")
51
+ end
52
+
53
+ it "handles missing pages" do
54
+ request = Net::HTTP::Get.new("/missing")
55
+ response = client.request(request)
56
+ response.code.should eq("404")
57
+ response["Content-Type"].should eq("text/html")
58
+ end
59
+
60
+ it "handles empty response bodies" do
61
+ request = Net::HTTP::Post.new("/test")
62
+ request.body = ""
63
+ response = client.request(request)
64
+ response.code.should eq("204")
65
+ # FIXME: Mongrel/WEBrick fail this test. Is there a bug?
66
+ #response["Content-Type"].should be_nil
67
+ response["Content-Length"].should be_nil
68
+ response.body.should be_nil
69
+ end
70
+
71
+ it "handles string response bodies" do
72
+ request = Net::HTTP::Get.new("/test")
73
+ request["Accept"] = "test/response.stringbody"
74
+ response = client.request(request)
75
+ response["Content-Length"].should eq("20")
76
+ response.body.should eq("String response body")
77
+ end
78
+
79
+ it "handles enumerable response bodies" do
80
+ request = Net::HTTP::Get.new("/test")
81
+ request["Accept"] = "test/response.enumbody"
82
+ response = client.request(request)
83
+ response["Transfer-Encoding"].should eq("chunked")
84
+ response.body.should eq("Enumerable response body")
85
+ end
86
+
87
+ it "handles proc response bodies" do
88
+ request = Net::HTTP::Get.new("/test")
89
+ request["Accept"] = "test/response.procbody"
90
+ response = client.request(request)
91
+ response["Transfer-Encoding"].should eq("chunked")
92
+ response.body.should eq("Proc response body")
93
+ end
94
+
95
+ it "handles fiber response bodies" do
96
+ request = Net::HTTP::Get.new("/test")
97
+ request["Accept"] = "test/response.fiberbody"
98
+ response = client.request(request)
99
+ response["Transfer-Encoding"].should eq("chunked")
100
+ response.body.should eq("Fiber response body")
101
+ end
102
+
103
+ it "handles io response bodies" do
104
+ request = Net::HTTP::Get.new("/test")
105
+ request["Accept"] = "test/response.iobody"
106
+ response = client.request(request)
107
+ response["Content-Length"].should eq("16")
108
+ response.body.should eq("IO response body")
109
+ end
110
+
111
+ it "handles request cookies" do
112
+ request = Net::HTTP::Get.new("/test")
113
+ request["Accept"] = "test/response.cookies"
114
+ request["Cookie"] = "echo=echocookie"
115
+ response = client.request(request)
116
+ response.body.should eq("echocookie")
117
+ end
118
+
119
+ it "handles response cookies" do
120
+ request = Net::HTTP::Get.new("/test")
121
+ request["Accept"] = "test/response.cookies"
122
+ response = client.request(request)
123
+ response["Set-Cookie"].should eq("cookie=monster, rodeo=clown")
124
+ end
125
+ end