webmachine 1.2.2 → 1.6.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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/CHANGELOG.md +57 -0
  4. data/Gemfile +20 -15
  5. data/README.md +89 -91
  6. data/RELEASING.md +21 -0
  7. data/Rakefile +5 -21
  8. data/documentation/adapters.md +41 -0
  9. data/documentation/authentication-and-authorization.md +37 -0
  10. data/documentation/configurator.md +19 -0
  11. data/documentation/error-handling.md +86 -0
  12. data/documentation/examples.md +224 -0
  13. data/documentation/how-it-works.md +76 -0
  14. data/documentation/routes.md +112 -0
  15. data/documentation/validation.md +159 -0
  16. data/documentation/versioning-apis.md +74 -0
  17. data/documentation/visual-debugger.md +38 -0
  18. data/examples/application.rb +2 -2
  19. data/examples/debugger.rb +1 -1
  20. data/lib/webmachine.rb +3 -1
  21. data/lib/webmachine/adapter.rb +7 -13
  22. data/lib/webmachine/adapters.rb +1 -2
  23. data/lib/webmachine/adapters/httpkit.rb +74 -0
  24. data/lib/webmachine/adapters/lazy_request_body.rb +1 -2
  25. data/lib/webmachine/adapters/rack.rb +70 -25
  26. data/lib/webmachine/adapters/rack_mapped.rb +42 -0
  27. data/lib/webmachine/adapters/reel.rb +22 -23
  28. data/lib/webmachine/adapters/webrick.rb +16 -16
  29. data/lib/webmachine/application.rb +2 -2
  30. data/lib/webmachine/chunked_body.rb +3 -4
  31. data/lib/webmachine/configuration.rb +1 -1
  32. data/lib/webmachine/constants.rb +75 -0
  33. data/lib/webmachine/decision/conneg.rb +12 -10
  34. data/lib/webmachine/decision/flow.rb +42 -32
  35. data/lib/webmachine/decision/fsm.rb +14 -21
  36. data/lib/webmachine/decision/helpers.rb +10 -38
  37. data/lib/webmachine/dispatcher.rb +13 -10
  38. data/lib/webmachine/dispatcher/route.rb +45 -9
  39. data/lib/webmachine/errors.rb +9 -3
  40. data/lib/webmachine/events.rb +2 -2
  41. data/lib/webmachine/header_negotiation.rb +25 -0
  42. data/lib/webmachine/headers.rb +8 -3
  43. data/lib/webmachine/locale/en.yml +7 -5
  44. data/lib/webmachine/media_type.rb +10 -8
  45. data/lib/webmachine/request.rb +67 -26
  46. data/lib/webmachine/rescueable_exception.rb +62 -0
  47. data/lib/webmachine/resource.rb +1 -1
  48. data/lib/webmachine/resource/callbacks.rb +11 -9
  49. data/lib/webmachine/response.rb +3 -5
  50. data/lib/webmachine/spec/IO_response.body +1 -0
  51. data/lib/webmachine/spec/adapter_lint.rb +83 -37
  52. data/lib/webmachine/spec/test_resource.rb +15 -4
  53. data/lib/webmachine/streaming/fiber_encoder.rb +1 -5
  54. data/lib/webmachine/streaming/io_encoder.rb +7 -1
  55. data/lib/webmachine/trace.rb +1 -0
  56. data/lib/webmachine/trace/fsm.rb +20 -10
  57. data/lib/webmachine/trace/resource_proxy.rb +2 -0
  58. data/lib/webmachine/translation.rb +2 -1
  59. data/lib/webmachine/version.rb +3 -3
  60. data/memory_test.rb +37 -0
  61. data/spec/spec_helper.rb +17 -9
  62. data/spec/webmachine/adapter_spec.rb +14 -15
  63. data/spec/webmachine/adapters/httpkit_spec.rb +10 -0
  64. data/spec/webmachine/adapters/rack_mapped_spec.rb +71 -0
  65. data/spec/webmachine/adapters/rack_spec.rb +32 -6
  66. data/spec/webmachine/adapters/reel_spec.rb +16 -12
  67. data/spec/webmachine/adapters/webrick_spec.rb +2 -2
  68. data/spec/webmachine/application_spec.rb +18 -17
  69. data/spec/webmachine/chunked_body_spec.rb +3 -3
  70. data/spec/webmachine/configuration_spec.rb +5 -5
  71. data/spec/webmachine/cookie_spec.rb +13 -13
  72. data/spec/webmachine/decision/conneg_spec.rb +49 -43
  73. data/spec/webmachine/decision/falsey_spec.rb +4 -4
  74. data/spec/webmachine/decision/flow_spec.rb +195 -145
  75. data/spec/webmachine/decision/fsm_spec.rb +81 -19
  76. data/spec/webmachine/decision/helpers_spec.rb +20 -20
  77. data/spec/webmachine/dispatcher/rfc3986_percent_decode_spec.rb +22 -0
  78. data/spec/webmachine/dispatcher/route_spec.rb +114 -32
  79. data/spec/webmachine/dispatcher_spec.rb +49 -24
  80. data/spec/webmachine/errors_spec.rb +1 -1
  81. data/spec/webmachine/etags_spec.rb +19 -19
  82. data/spec/webmachine/events_spec.rb +6 -6
  83. data/spec/webmachine/headers_spec.rb +14 -14
  84. data/spec/webmachine/media_type_spec.rb +36 -36
  85. data/spec/webmachine/request_spec.rb +70 -39
  86. data/spec/webmachine/rescueable_exception_spec.rb +15 -0
  87. data/spec/webmachine/resource/authentication_spec.rb +6 -6
  88. data/spec/webmachine/response_spec.rb +18 -12
  89. data/spec/webmachine/trace/fsm_spec.rb +8 -8
  90. data/spec/webmachine/trace/resource_proxy_spec.rb +9 -9
  91. data/spec/webmachine/trace/trace_store_spec.rb +5 -5
  92. data/spec/webmachine/trace_spec.rb +3 -3
  93. data/webmachine.gemspec +2 -6
  94. metadata +78 -228
  95. data/lib/webmachine/adapters/hatetepe.rb +0 -108
  96. data/lib/webmachine/adapters/mongrel.rb +0 -127
  97. data/lib/webmachine/dispatcher/not_found_resource.rb +0 -5
  98. data/lib/webmachine/fiber18.rb +0 -88
  99. data/spec/webmachine/adapters/hatetepe_spec.rb +0 -60
  100. data/spec/webmachine/adapters/mongrel_spec.rb +0 -16
@@ -0,0 +1,112 @@
1
+ # Routes
2
+
3
+ ## Paths
4
+
5
+ ```ruby
6
+ App = Webmachine::Application.new do |app|
7
+ app.routes do
8
+ # Will map to /orders
9
+ add ["orders"], OrdersResource
10
+
11
+ # Will map to /orders/:id
12
+ # request.path_info[:id] will contain the matched token value
13
+ add ["orders", :id], OrderResource
14
+
15
+ # Will map to /person/:person_id/orders/:order_id and
16
+ # provide :person_id and :order_id in request.path_info
17
+ add ["person", :person_id, "orders", :order_id], OrderResource
18
+
19
+ # Will map to any path starting with /orders,
20
+ # but will not provide any path_info
21
+ add ["orders", :*], OrderResource
22
+
23
+ # Will map to any path that matches the given components and regular expression
24
+ # Any capture groups specified in the regex will be made available in
25
+ # request.path_info[:captures. In this case, you would get one or two
26
+ # values in :captures depending on whether your request looked like:
27
+ # /orders/1/cancel
28
+ # or
29
+ # /orders/1/cancel.json
30
+ add ["orders", :id, /([^.]*)\.?(.*)?/], OrderResource
31
+
32
+ # You can even use named captures with regular expressions. This will
33
+ # automatically put the captures into path_info. In the below example,
34
+ # you would get :id from the symbol, along with :action and :format
35
+ # from the regex. :format in this case would be optional.
36
+ add ["orders", :id, /(?<action>)[^.]*)\.?(?<format>.*)?/], OrderResource
37
+
38
+ # will map to any path
39
+ add [:*], DefaultResource
40
+ end
41
+ end
42
+ ```
43
+
44
+ ## Guards
45
+
46
+ Guards prevent a request being sent to a Resource with a matching route unless its conditions are met.
47
+
48
+ ##### Lambda
49
+
50
+ ```ruby
51
+ App = Webmachine::Application.new do |app|
52
+ app.routes do
53
+ add ["orders"], lambda { |request| request.headers['X-My-App-Version'] == "1" }, OrdersResourceV1
54
+ add ["orders"], lambda { |request| request.headers['X-My-App-Version'] == "2" }, OrdersResourceV2
55
+ end
56
+ end
57
+
58
+ ```
59
+
60
+ ##### Block
61
+
62
+ ```ruby
63
+ App = Webmachine::Application.new do |app|
64
+ app.routes do
65
+ add ["orders"], OrdersResourceV1 do | request |
66
+ request.headers['X-My-App-Version'] == "1"
67
+ end
68
+ end
69
+ end
70
+
71
+ ```
72
+
73
+ ##### Callable class
74
+
75
+ ```ruby
76
+ class VersionGuard
77
+
78
+ def initialize version
79
+ @version = version
80
+ end
81
+
82
+ def call(request)
83
+ request.headers['X-My-App-Version'] == @version
84
+ end
85
+
86
+ end
87
+
88
+ App = Webmachine::Application.new do |app|
89
+ app.routes do
90
+ add ["orders"], VersionGuard.new("1"), OrdersResourceV1
91
+ add ["orders"], VersionGuard.new("2"), OrdersResourceV2
92
+ end
93
+ end
94
+
95
+ ```
96
+
97
+ ## User defined bindings
98
+
99
+ User defined bindings specified for a route will be made available through `request.path_info`.
100
+
101
+ ```ruby
102
+
103
+ App = Webmachine::Application.new do |app|
104
+ app.routes do
105
+ add ["orders"], OrdersResource, :foo => "bar"
106
+ end
107
+ end
108
+
109
+ request.path_info[:foo]
110
+ => "bar"
111
+
112
+ ```
@@ -0,0 +1,159 @@
1
+ # Validation
2
+
3
+ There are a couple of callbacks that are the most appropriate for doing validation in. The first is the `malformed_request?` which runs early in the Finite State Machine, and the second is inside the content type handler, for example `from_json`.
4
+
5
+ ## malformed_request
6
+
7
+ If `malformed_request?` returns a truthy value, then a 400 Bad Request will be returned. Unfortunately, at this early stage in the flow, we don't know what the `method` or the `Content-Type` are without inspecting the request, and this leads to some very Iffy code.
8
+
9
+ ```ruby
10
+ class OrdersResource < Webmachine::Resource
11
+
12
+ def allowed_methods
13
+ ["POST", "GET"]
14
+ end
15
+
16
+ # Iffy! What method? GET doesn't require any validation.
17
+ def malformed_request?
18
+ if request.post?
19
+ # What Content-Type? Very Iffy!
20
+ if request.headers['Content-Type'] == "application/json"
21
+ ....
22
+ end
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def content_types_accepted
29
+ [
30
+ ["application/json", :from_json],
31
+ ["application/xml", :from_xml]
32
+ ]
33
+ end
34
+
35
+ def content_types_provided
36
+ [
37
+ ["application/json", :to_json],
38
+ ["application/xml", :to_xml]
39
+ ]
40
+ end
41
+
42
+ def post_is_create?
43
+ true
44
+ end
45
+
46
+ def create_path
47
+ "/orders/#{next_id}"
48
+ end
49
+
50
+ def from_json
51
+ order = Order.from_json(request.body.to_s)
52
+ response.body = order.save(next_id).to_json
53
+ end
54
+
55
+ def from_xml
56
+ order = Order.from_xml(request.body.to_s)
57
+ response.body = order.save(next_id).to_xml
58
+ end
59
+
60
+ def to_json
61
+ Order.all.to_json
62
+ end
63
+
64
+ def to_xml
65
+ Order.all.to_xml
66
+ end
67
+
68
+ private
69
+
70
+ def next_id
71
+ @next_id ||= Order.next_id
72
+ end
73
+
74
+ end
75
+ ```
76
+
77
+ ## Content-Type handler
78
+
79
+ A more elegant way to handle validation is to do it in a callback where we already know the `method` and the `Content-Type` - that is, the handler for the given Content-Type (eg. `from_json` and `from_xml`). By returning a `400` from the handler, we stop the state machine flow.
80
+
81
+ ```ruby
82
+ class OrdersResource < Webmachine::Resource
83
+
84
+ def allowed_methods
85
+ ["POST", "GET"]
86
+ end
87
+
88
+ # Iffy! What method? GET doesn't require any validation.
89
+ def malformed_request?
90
+ if request.post?
91
+ invalid_create_order_request?
92
+ else
93
+ false
94
+ end
95
+ end
96
+
97
+ def content_types_accepted
98
+ [
99
+ ["application/json", :from_json],
100
+ ["application/xml", :from_xml]
101
+ ]
102
+ end
103
+
104
+ def content_types_provided
105
+ [
106
+ ["application/json", :to_json],
107
+ ["application/xml", :to_xml]
108
+ ]
109
+ end
110
+
111
+ def post_is_create?
112
+ true
113
+ end
114
+
115
+ def create_path
116
+ "/orders/#{next_id}"
117
+ end
118
+
119
+ def from_json
120
+ order = Order.from_json(request.body.to_s)
121
+ # A bit less Iffy!
122
+ return json_validation_errors(order) unless order.valid?
123
+ response.body = order.save(next_id).to_json
124
+ end
125
+
126
+ # This could use some DRYing up, but you get the point.
127
+ def from_xml
128
+ order = Order.from_xml(request.body.to_s)
129
+ # A bit less Iffy!
130
+ return xml_validation_errors(order) unless order.valid?
131
+ response.body = order.save(next_id).to_xml
132
+ end
133
+
134
+ def to_json
135
+ Order.all.to_json
136
+ end
137
+
138
+ def to_xml
139
+ Order.all.to_xml
140
+ end
141
+
142
+ private
143
+
144
+ def json_validation_errors(order)
145
+ response.body = order.validation_errors.to_json
146
+ 400
147
+ end
148
+
149
+ def xml_validation_errors(order)
150
+ response.body = order.validation_errors.to_xml
151
+ 400
152
+ end
153
+
154
+ def next_id
155
+ @next_id ||= Order.next_id
156
+ end
157
+
158
+ end
159
+ ```
@@ -0,0 +1,74 @@
1
+ # Versioning APIs
2
+
3
+ ## By URL
4
+
5
+ ```ruby
6
+
7
+ class MyResourceV1 < Webmachine::Resource
8
+
9
+ end
10
+
11
+ class MyResourceV2 < Webmachine::Resource
12
+
13
+ end
14
+
15
+ App = Webmachine::Application.new do |app|
16
+ app.routes do
17
+ add ["api", "v1", "myresource"], MyResourceV1
18
+ add ["api", "v2", "myresource"], MyResourceV2
19
+ end
20
+ end
21
+
22
+ ```
23
+
24
+ ## By Content-Type
25
+
26
+ Note: if no Accept header is specified, then the first content type in the list will be chosen.
27
+
28
+ ```ruby
29
+
30
+ class MyResource < Webmachine::Resource
31
+
32
+ def content_types_provided
33
+ [
34
+ ["application/myapp.v2+json", :to_json_v2],
35
+ ["application/myapp.v1+json", :to_json_v1]
36
+ ]
37
+ end
38
+
39
+ end
40
+
41
+ ```
42
+
43
+ ## By Header value
44
+
45
+ ```ruby
46
+
47
+ class MyResourceV1 < Webmachine::Resource
48
+
49
+ end
50
+
51
+ class MyResourceV2 < Webmachine::Resource
52
+
53
+ end
54
+
55
+ class VersionGuard
56
+
57
+ def initialize version
58
+ @version = version
59
+ end
60
+
61
+ def call(request)
62
+ request.headers['X-My-App-Version'] == @version
63
+ end
64
+
65
+ end
66
+
67
+ App = Webmachine::Application.new do |app|
68
+ app.routes do
69
+ add ["api", "myresource"], VersionGuard.new("1"), MyResourceV1
70
+ add ["api", "myresource"], VersionGuard.new("2"), MyResourceV2
71
+ end
72
+ end
73
+
74
+ ```
@@ -0,0 +1,38 @@
1
+ ### Visual debugger
2
+
3
+ In development, you can turn on tracing of the
4
+ decision graph for a resource by implementing the `#trace?` callback
5
+ so that it returns true:
6
+
7
+ ```ruby
8
+ class MyTracedResource < Webmachine::Resource
9
+ def trace?
10
+ true
11
+ end
12
+
13
+ # The rest of your callbacks...
14
+ end
15
+ ```
16
+
17
+ Then enable the visual debugger resource by adding a route to your
18
+ configuration:
19
+
20
+ ```ruby
21
+ Webmachine.application.routes do
22
+ # This can be any path as long as it ends with :*
23
+ add ['trace', :*], Webmachine::Trace::TraceResource
24
+ # The rest of your routes...
25
+ end
26
+ ```
27
+
28
+ Now when you visit your traced resource, a trace of the request
29
+ process will be recorded in memory. Open your browser to `/trace` to
30
+ list the recorded traces and inspect the result. The response from your
31
+ traced resource will also include the `X-Webmachine-Trace-Id` that you
32
+ can use to lookup the trace. It might look something like this:
33
+
34
+ ![preview calls at decision](http://seancribbs-skitch.s3.amazonaws.com/Webmachine_Trace_2156885920-20120625-100153.png)
35
+
36
+ Refer to
37
+ [examples/debugger.rb](/examples/debugger.rb)
38
+ for an example of how to enable the debugger.
@@ -25,10 +25,10 @@ MyApp = Webmachine::Application.new do |app|
25
25
  config.adapter = :WEBrick
26
26
  end
27
27
  # And add routes like this:
28
- app.add_route ['fizz', :buzz, '*'], RouteDebugResource
28
+ app.add_route ['fizz', :buzz, :*], RouteDebugResource
29
29
  # OR add routes this way:
30
30
  app.routes do
31
- add [:test, :foo, '*'], RouteDebugResource
31
+ add [:test, :foo, :*], RouteDebugResource
32
32
  end
33
33
  end
34
34
 
data/examples/debugger.rb CHANGED
@@ -31,7 +31,7 @@ end
31
31
 
32
32
  TraceExample = Webmachine::Application.new do |app|
33
33
  app.routes do
34
- add ['trace', '*'], Webmachine::Trace::TraceResource
34
+ add ['trace', :*], Webmachine::Trace::TraceResource
35
35
  add [], MyTracedResource
36
36
  end
37
37
  end
data/lib/webmachine.rb CHANGED
@@ -1,10 +1,12 @@
1
- require 'webmachine/configuration'
1
+ require 'webmachine/configuration'
2
+ require 'webmachine/constants'
2
3
  require 'webmachine/cookie'
3
4
  require 'webmachine/headers'
4
5
  require 'webmachine/request'
5
6
  require 'webmachine/response'
6
7
  require 'webmachine/etags'
7
8
  require 'webmachine/errors'
9
+ require 'webmachine/header_negotiation'
8
10
  require 'webmachine/decision'
9
11
  require 'webmachine/streaming'
10
12
  require 'webmachine/adapter'
@@ -5,23 +5,17 @@ module Webmachine
5
5
  # @abstract Subclass and override {#run} to implement a custom adapter.
6
6
  class Adapter
7
7
 
8
- # @return [Webmachine::Configuration] the application's configuration.
9
- attr_reader :configuration
8
+ # @return [Webmachine::Application] returns the application
9
+ attr_reader :application
10
10
 
11
- # @return [Webmachine::Dispatcher] the application's dispatcher.
12
- attr_reader :dispatcher
13
-
14
- # @param [Webmachine::Configuration] configuration the application's
15
- # configuration.
16
- # @param [Webmachine::Dispatcher] dispatcher the application's dispatcher.
17
- def initialize(configuration, dispatcher)
18
- @configuration = configuration
19
- @dispatcher = dispatcher
11
+ # @param [Webmachine::Application] application the application
12
+ def initialize(application)
13
+ @application = application
20
14
  end
21
15
 
22
16
  # Create a new adapter and run it.
23
- def self.run(configuration, dispatcher)
24
- new(configuration, dispatcher).run
17
+ def self.run(application)
18
+ new(application).run
25
19
  end
26
20
 
27
21
  # Start the adapter.