webmachine 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/CHANGELOG.md +4 -0
  4. data/Gemfile +13 -11
  5. data/README.md +85 -89
  6. data/Rakefile +0 -1
  7. data/documentation/adapters.md +39 -0
  8. data/documentation/authentication-and-authorization.md +37 -0
  9. data/documentation/configurator.md +19 -0
  10. data/documentation/error-handling.md +86 -0
  11. data/documentation/examples.md +215 -0
  12. data/documentation/how-it-works.md +76 -0
  13. data/documentation/routes.md +97 -0
  14. data/documentation/validation.md +159 -0
  15. data/documentation/versioning-apis.md +74 -0
  16. data/documentation/visual-debugger.md +38 -0
  17. data/examples/application.rb +2 -2
  18. data/examples/debugger.rb +1 -1
  19. data/lib/webmachine.rb +3 -1
  20. data/lib/webmachine/adapter.rb +7 -13
  21. data/lib/webmachine/adapters.rb +1 -2
  22. data/lib/webmachine/adapters/httpkit.rb +74 -0
  23. data/lib/webmachine/adapters/lazy_request_body.rb +1 -2
  24. data/lib/webmachine/adapters/rack.rb +37 -21
  25. data/lib/webmachine/adapters/reel.rb +21 -23
  26. data/lib/webmachine/adapters/webrick.rb +16 -16
  27. data/lib/webmachine/application.rb +2 -2
  28. data/lib/webmachine/chunked_body.rb +3 -4
  29. data/lib/webmachine/constants.rb +75 -0
  30. data/lib/webmachine/decision/conneg.rb +12 -10
  31. data/lib/webmachine/decision/flow.rb +31 -21
  32. data/lib/webmachine/decision/fsm.rb +10 -18
  33. data/lib/webmachine/decision/helpers.rb +9 -37
  34. data/lib/webmachine/dispatcher.rb +13 -10
  35. data/lib/webmachine/dispatcher/route.rb +18 -8
  36. data/lib/webmachine/errors.rb +7 -1
  37. data/lib/webmachine/header_negotiation.rb +25 -0
  38. data/lib/webmachine/headers.rb +7 -2
  39. data/lib/webmachine/locale/en.yml +7 -5
  40. data/lib/webmachine/media_type.rb +10 -8
  41. data/lib/webmachine/request.rb +44 -15
  42. data/lib/webmachine/resource.rb +1 -1
  43. data/lib/webmachine/resource/callbacks.rb +6 -4
  44. data/lib/webmachine/spec/IO_response.body +1 -0
  45. data/lib/webmachine/spec/adapter_lint.rb +70 -36
  46. data/lib/webmachine/spec/test_resource.rb +10 -4
  47. data/lib/webmachine/streaming/fiber_encoder.rb +1 -5
  48. data/lib/webmachine/streaming/io_encoder.rb +6 -0
  49. data/lib/webmachine/trace.rb +1 -0
  50. data/lib/webmachine/trace/fsm.rb +20 -10
  51. data/lib/webmachine/trace/resource_proxy.rb +2 -0
  52. data/lib/webmachine/translation.rb +2 -1
  53. data/lib/webmachine/version.rb +3 -3
  54. data/memory_test.rb +37 -0
  55. data/spec/spec_helper.rb +9 -9
  56. data/spec/webmachine/adapter_spec.rb +14 -15
  57. data/spec/webmachine/adapters/httpkit_spec.rb +10 -0
  58. data/spec/webmachine/adapters/rack_spec.rb +6 -6
  59. data/spec/webmachine/adapters/reel_spec.rb +15 -11
  60. data/spec/webmachine/adapters/webrick_spec.rb +2 -2
  61. data/spec/webmachine/application_spec.rb +18 -17
  62. data/spec/webmachine/chunked_body_spec.rb +3 -3
  63. data/spec/webmachine/configuration_spec.rb +5 -5
  64. data/spec/webmachine/cookie_spec.rb +13 -13
  65. data/spec/webmachine/decision/conneg_spec.rb +48 -42
  66. data/spec/webmachine/decision/falsey_spec.rb +4 -4
  67. data/spec/webmachine/decision/flow_spec.rb +194 -144
  68. data/spec/webmachine/decision/fsm_spec.rb +17 -17
  69. data/spec/webmachine/decision/helpers_spec.rb +20 -20
  70. data/spec/webmachine/dispatcher/route_spec.rb +73 -27
  71. data/spec/webmachine/dispatcher_spec.rb +34 -24
  72. data/spec/webmachine/errors_spec.rb +1 -1
  73. data/spec/webmachine/etags_spec.rb +19 -19
  74. data/spec/webmachine/events_spec.rb +6 -6
  75. data/spec/webmachine/headers_spec.rb +14 -14
  76. data/spec/webmachine/media_type_spec.rb +36 -36
  77. data/spec/webmachine/request_spec.rb +33 -33
  78. data/spec/webmachine/resource/authentication_spec.rb +6 -6
  79. data/spec/webmachine/response_spec.rb +12 -12
  80. data/spec/webmachine/trace/fsm_spec.rb +8 -8
  81. data/spec/webmachine/trace/resource_proxy_spec.rb +9 -9
  82. data/spec/webmachine/trace/trace_store_spec.rb +5 -5
  83. data/spec/webmachine/trace_spec.rb +3 -3
  84. data/webmachine.gemspec +2 -6
  85. metadata +48 -206
  86. data/lib/webmachine/adapters/hatetepe.rb +0 -108
  87. data/lib/webmachine/adapters/mongrel.rb +0 -127
  88. data/lib/webmachine/dispatcher/not_found_resource.rb +0 -5
  89. data/lib/webmachine/fiber18.rb +0 -88
  90. data/spec/webmachine/adapters/hatetepe_spec.rb +0 -60
  91. data/spec/webmachine/adapters/mongrel_spec.rb +0 -16
@@ -0,0 +1,215 @@
1
+ Imagine an application with an "orders" resource, `OrdersResource`, that represents the collection of orders in the application, and an "order" resource, `OrderResource`, that represents a single order object.
2
+
3
+ This is how the /orders and /orders/:id routes are mapped to their respective resource classes.
4
+
5
+ ```ruby
6
+ App = Webmachine::Application.new do |app|
7
+ app.routes do
8
+ add ["orders"], OrdersResource
9
+ add ["orders", :id], OrderResource
10
+ end
11
+ end
12
+ ```
13
+
14
+ # GET
15
+ * Override `resource_exists?`, `content_types_provided`, `allowed_methods`, and implement the method to render the resource.
16
+
17
+ Curious as to which order the callbacks will be invoked in? Read why it [doesn't have to matter](#callback-order).
18
+
19
+ ```ruby
20
+ class OrderResource < Webmachine::Resource
21
+ def allowed_methods
22
+ ["GET"]
23
+ end
24
+
25
+ def content_types_provided
26
+ [["application/json", :to_json]]
27
+ end
28
+
29
+ def resource_exists?
30
+ order
31
+ end
32
+
33
+ def to_json
34
+ order.to_json
35
+ end
36
+
37
+ private
38
+
39
+ def order
40
+ @order ||= Order.new(params)
41
+ end
42
+
43
+ def id
44
+ request.path_info[:id]
45
+ end
46
+ end
47
+
48
+ ```
49
+
50
+ # POST to create a new resource in a collection
51
+ * Override `post_is_create?` to return true
52
+ * Override `create_path` to return the relative path to the new resource. Note that `create_path` will be called _before_ the content type handler (eg. `from_json`) is called, which means that you need to know the ID before the object has been inserted into the database. This might seem a hassle, but it stops you from exposing your database column IDs to the world, which is a naughty and lazy habit we've all picked up from Rails.
53
+ * The response Content-Type and status will be set for you.
54
+
55
+ ```ruby
56
+ class OrdersResource < Webmachine::Resource
57
+
58
+ def allowed_methods
59
+ ["POST"]
60
+ end
61
+
62
+ def content_types_accepted
63
+ [["application/json", :from_json]]
64
+ end
65
+
66
+ def post_is_create?
67
+ true
68
+ end
69
+
70
+ def create_path
71
+ "/orders/#{next_id}"
72
+ end
73
+
74
+ private
75
+
76
+ def from_json
77
+ response.body = new_order.save(next_id).to_json
78
+ end
79
+
80
+ def next_id
81
+ @id ||= Order.next_id
82
+ end
83
+
84
+ def new_order
85
+ @new_order ||= Order.new(params)
86
+ end
87
+
88
+ def params
89
+ JSON.parse(request.body.to_s)
90
+ end
91
+ end
92
+ ```
93
+
94
+ # POST to perform a task
95
+ * Override `allowed_methods` and `process_post`. Put all the code to be executed in `process_post`.
96
+ * `process_post` must return true, or the HTTP response code
97
+ * Response headers like Content-Type will need to be set manually.
98
+
99
+ ```ruby
100
+ class DispatchOrderResource < Webmachine::Resource
101
+
102
+ def allowed_methods
103
+ ["POST"]
104
+ end
105
+
106
+ def resource_exists?
107
+ @order = Order.find(id)
108
+ end
109
+
110
+ def process_post
111
+ @order.dispatch
112
+ response.headers['Content-Type'] = 'text/plain'
113
+ response.body = "Successfully dispatched order #{id}"
114
+ true
115
+ end
116
+
117
+ private
118
+
119
+ def id
120
+ request.path_info[:id]
121
+ end
122
+ end
123
+
124
+ ```
125
+
126
+ # PUT
127
+ * Override `resource_exists?`, `content_types_accepted`, `allowed_methods`, and implement the method to create/replace the resource.
128
+
129
+ ```ruby
130
+ class OrderResource < Webmachine::Resource
131
+
132
+ def allowed_methods
133
+ ["PUT"]
134
+ end
135
+
136
+ def content_types_accepted
137
+ [["application/json", :from_json]]
138
+ end
139
+
140
+ def resource_exists?
141
+ order
142
+ end
143
+
144
+ def from_json
145
+ # Remember PUT should replace the entire resource, not merge the attributes! That's what PATCH is for.
146
+ # It's also why you should not expose your database IDs as your API IDs.
147
+ order.destroy if order
148
+ new_order = Order.new(params)
149
+ new_order.save(id)
150
+ response.body = new_order.to_json
151
+ end
152
+
153
+ private
154
+
155
+ def order
156
+ @order ||= Order.find(id)
157
+ end
158
+
159
+ def params
160
+ JSON.parse(request.body.to_s)
161
+ end
162
+
163
+ def id
164
+ request.path_info[:id]
165
+ end
166
+ end
167
+ ```
168
+
169
+ # PATCH
170
+ * Webmachine does not currently support PATCH requests. See https://github.com/seancribbs/webmachine-ruby/issues/109 for more information and https://github.com/bethesque/pact_broker/blob/2918814e70bbda14df68598a6a41502a5eac4308/lib/pact_broker/api/resources/pacticipant.rb for a dirty hack to make it work if you need to.
171
+
172
+ # DELETE
173
+ * Override `resource_exists?` and `delete_resource`
174
+ * `delete_resource` must return true
175
+ * See callbacks.rb for documentation on asynchronous deletes.
176
+
177
+ ```ruby
178
+ class OrderResource < Webmachine::Resource
179
+
180
+ def allowed_methods
181
+ ["DELETE"]
182
+ end
183
+
184
+ def resource_exists?
185
+ order
186
+ end
187
+
188
+ def delete_resource
189
+ order.destroy
190
+ true
191
+ end
192
+
193
+ private
194
+
195
+ def order
196
+ @order ||= Order.find(id)
197
+ end
198
+
199
+ def id
200
+ request.path_info[:id]
201
+ end
202
+
203
+ end
204
+ ```
205
+
206
+ Thanks to [oestrich][oestrich] for putting together the original example. You can see the full source code [here][source].
207
+
208
+ [oestrich]: https://github.com/oestrich
209
+ [source]: https://gist.github.com/oestrich/3638605
210
+
211
+ <a name="callback-order">
212
+ ## What order are the callbacks invoked in?
213
+ </a>
214
+
215
+ This question is actually irrelevant if you write your code in a "stateless" way using lazy initialization as the examples do above. As much as possible, think about exposing "facts" about your resource, not writing procedural code that needs to be called in a certain order. See [How it works](/documentation/how-it-works.md) for more information on how the Webmachine state machine works.
@@ -0,0 +1,76 @@
1
+ ### How it works
2
+
3
+ Unlike frameworks like Grape and Sinatra, which create a response by running a predefined procedure when a request is received, Webmachine creates an HTTP response by determining a series of "facts" about the resource.
4
+
5
+ Webmachine is implemented as a [Finite State Machine][diagram]. It uses the facts about your resource to determine the flow though the FSM in order to produce a response.
6
+
7
+ As an example, imagine the following request:
8
+
9
+ $ curl "http://example.org/widgets/1" -H "Accept: application/json" -u jsmith -p secret
10
+
11
+ The series of "facts" that Webmachine will determine as it moves through the state machine for this request include:
12
+
13
+ * Does the route /widgets/:id exist?
14
+ * Does a widget with ID 1 exist?
15
+ * Is jsmith with the given password allowed to execute this HTTP method on this widget?
16
+ * Can the GET method be called for a widget?
17
+ * Can a widget be rendered as application/json?
18
+
19
+ If the answer to each of these questions is "yes", then Webmachine will ask the final question - please render the response for me. If the answer to any of the questions along the way is "no", then the appropriate HTTP response code will be returned automatically.
20
+
21
+ ## Creating a resource
22
+
23
+ * The way you tell Webmachine the facts about your resource is to create a class that extends Webmachine::Resource, and override the relevant callbacks. For example, what content types it provides (`content_types_accepted`), what HTTP methods it supports (`allowed_methods`), whether or not it exists (`resource_exists?`), and how the resource should be rendered (`to_json`). See the [examples][examples] page for examples of how to implement support for each HTTP method.
24
+
25
+ ```ruby
26
+ class WidgetResource < Webmachine::Resource
27
+
28
+ def allowed_methods
29
+ ["GET"]
30
+ end
31
+
32
+ def content_types_provided
33
+ [["application/json", :to_json]]
34
+ end
35
+
36
+ def resource_exists?
37
+ widget # Truthy or falsey
38
+ end
39
+
40
+ def to_json
41
+ widget.to_json
42
+ end
43
+
44
+ private
45
+
46
+ def widget
47
+ @widget ||= Widget.find(id)
48
+ end
49
+
50
+ def id
51
+ request.path_info[:id]
52
+ end
53
+
54
+ end
55
+ ```
56
+
57
+ * To see a list of the callbacks that can be overridden, and documentation about how to override each one, check out the [Callbacks][callbacks] class.
58
+
59
+ * Callbacks that have a name with a question mark should return a truthy or falsey value, or an integer response code.
60
+
61
+ * Most callbacks can interrupt the decision flow by returning an integer response code. You generally only want to do this when new information comes to light, requiring a modification of the response.
62
+
63
+ * Once an "end state" has been reached (for example, `resource_exists?` returns falsey to a GET request, or a callback returns an explicit response code), the FSM will stop the decision flow, and return the relevant response code to the client. The implication of this is that callbacks later in the flow (eg. the method to render the resource) can rely upon the fact that the resource's existence has already been proven, that authorisation has already been checked etc. so there is no need for any `if object == nil` type boilerplate.
64
+
65
+ ### Advanced
66
+
67
+ * Once you've seen how to implement a resource, the best way to get an understanding of how the FSM uses that resource is to check out the [Decision Flow Diagram][diagram] and then see how it is implemented in the [Flow][flow] class.
68
+
69
+ ### Guidelines
70
+
71
+ * A collection resource (eg. /orders) should be implemented as a separate class to a single object resource (eg. /orders/1), as the routes represent different underlying objects with different "facts". For example, the orders _collection_ resource probably always exists (but may be empty), however the order with ID 1 may or may not exist.
72
+
73
+ [callbacks]: https://github.com/seancribbs/webmachine-ruby/blob/master/lib/webmachine/resource/callbacks.rb
74
+ [diagram]: http://webmachine.basho.com/images/http-headers-status-v3.png
75
+ [flow]: https://github.com/seancribbs/webmachine-ruby/blob/master/lib/webmachine/decision/flow.rb
76
+ [examples]: /documentation/examples.md
@@ -0,0 +1,97 @@
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
24
+ add [:*], DefaultResource
25
+ end
26
+ end
27
+ ```
28
+
29
+ ## Guards
30
+
31
+ Guards prevent a request being sent to a Resource with a matching route unless its conditions are met.
32
+
33
+ ##### Lambda
34
+
35
+ ```ruby
36
+ App = Webmachine::Application.new do |app|
37
+ app.routes do
38
+ add ["orders"], lambda { |request| request.headers['X-My-App-Version'] == "1" }, OrdersResourceV1
39
+ add ["orders"], lambda { |request| request.headers['X-My-App-Version'] == "2" }, OrdersResourceV2
40
+ end
41
+ end
42
+
43
+ ```
44
+
45
+ ##### Block
46
+
47
+ ```ruby
48
+ App = Webmachine::Application.new do |app|
49
+ app.routes do
50
+ add ["orders"], OrdersResourceV1 do | request |
51
+ request.headers['X-My-App-Version'] == "1"
52
+ end
53
+ end
54
+ end
55
+
56
+ ```
57
+
58
+ ##### Callable class
59
+
60
+ ```ruby
61
+ class VersionGuard
62
+
63
+ def initialize version
64
+ @version = version
65
+ end
66
+
67
+ def call(request)
68
+ request.headers['X-My-App-Version'] == @version
69
+ end
70
+
71
+ end
72
+
73
+ App = Webmachine::Application.new do |app|
74
+ app.routes do
75
+ add ["orders"], VersionGuard.new("1"), OrdersResourceV1
76
+ add ["orders"], VersionGuard.new("2"), OrdersResourceV2
77
+ end
78
+ end
79
+
80
+ ```
81
+
82
+ ## User defined bindings
83
+
84
+ User defined bindings specified for a route will be made available through `request.path_info`.
85
+
86
+ ```ruby
87
+
88
+ App = Webmachine::Application.new do |app|
89
+ app.routes do
90
+ add ["orders"], OrdersResource, :foo => "bar"
91
+ end
92
+ end
93
+
94
+ request.path_info[:foo]
95
+ => "bar"
96
+
97
+ ```
@@ -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
+ ```