webmachine 1.1.0 → 1.2.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 (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