webmachine 1.2.2 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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.