webmachine 1.2.2 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +13 -11
- data/README.md +85 -89
- data/Rakefile +0 -1
- data/documentation/adapters.md +39 -0
- data/documentation/authentication-and-authorization.md +37 -0
- data/documentation/configurator.md +19 -0
- data/documentation/error-handling.md +86 -0
- data/documentation/examples.md +215 -0
- data/documentation/how-it-works.md +76 -0
- data/documentation/routes.md +97 -0
- data/documentation/validation.md +159 -0
- data/documentation/versioning-apis.md +74 -0
- data/documentation/visual-debugger.md +38 -0
- data/examples/application.rb +2 -2
- data/examples/debugger.rb +1 -1
- data/lib/webmachine.rb +3 -1
- data/lib/webmachine/adapter.rb +7 -13
- data/lib/webmachine/adapters.rb +1 -2
- data/lib/webmachine/adapters/httpkit.rb +74 -0
- data/lib/webmachine/adapters/lazy_request_body.rb +1 -2
- data/lib/webmachine/adapters/rack.rb +37 -21
- data/lib/webmachine/adapters/reel.rb +21 -23
- data/lib/webmachine/adapters/webrick.rb +16 -16
- data/lib/webmachine/application.rb +2 -2
- data/lib/webmachine/chunked_body.rb +3 -4
- data/lib/webmachine/constants.rb +75 -0
- data/lib/webmachine/decision/conneg.rb +12 -10
- data/lib/webmachine/decision/flow.rb +31 -21
- data/lib/webmachine/decision/fsm.rb +10 -18
- data/lib/webmachine/decision/helpers.rb +9 -37
- data/lib/webmachine/dispatcher.rb +13 -10
- data/lib/webmachine/dispatcher/route.rb +18 -8
- data/lib/webmachine/errors.rb +7 -1
- data/lib/webmachine/header_negotiation.rb +25 -0
- data/lib/webmachine/headers.rb +7 -2
- data/lib/webmachine/locale/en.yml +7 -5
- data/lib/webmachine/media_type.rb +10 -8
- data/lib/webmachine/request.rb +44 -15
- data/lib/webmachine/resource.rb +1 -1
- data/lib/webmachine/resource/callbacks.rb +6 -4
- data/lib/webmachine/spec/IO_response.body +1 -0
- data/lib/webmachine/spec/adapter_lint.rb +70 -36
- data/lib/webmachine/spec/test_resource.rb +10 -4
- data/lib/webmachine/streaming/fiber_encoder.rb +1 -5
- data/lib/webmachine/streaming/io_encoder.rb +6 -0
- data/lib/webmachine/trace.rb +1 -0
- data/lib/webmachine/trace/fsm.rb +20 -10
- data/lib/webmachine/trace/resource_proxy.rb +2 -0
- data/lib/webmachine/translation.rb +2 -1
- data/lib/webmachine/version.rb +3 -3
- data/memory_test.rb +37 -0
- data/spec/spec_helper.rb +9 -9
- data/spec/webmachine/adapter_spec.rb +14 -15
- data/spec/webmachine/adapters/httpkit_spec.rb +10 -0
- data/spec/webmachine/adapters/rack_spec.rb +6 -6
- data/spec/webmachine/adapters/reel_spec.rb +15 -11
- data/spec/webmachine/adapters/webrick_spec.rb +2 -2
- data/spec/webmachine/application_spec.rb +18 -17
- data/spec/webmachine/chunked_body_spec.rb +3 -3
- data/spec/webmachine/configuration_spec.rb +5 -5
- data/spec/webmachine/cookie_spec.rb +13 -13
- data/spec/webmachine/decision/conneg_spec.rb +48 -42
- data/spec/webmachine/decision/falsey_spec.rb +4 -4
- data/spec/webmachine/decision/flow_spec.rb +194 -144
- data/spec/webmachine/decision/fsm_spec.rb +17 -17
- data/spec/webmachine/decision/helpers_spec.rb +20 -20
- data/spec/webmachine/dispatcher/route_spec.rb +73 -27
- data/spec/webmachine/dispatcher_spec.rb +34 -24
- data/spec/webmachine/errors_spec.rb +1 -1
- data/spec/webmachine/etags_spec.rb +19 -19
- data/spec/webmachine/events_spec.rb +6 -6
- data/spec/webmachine/headers_spec.rb +14 -14
- data/spec/webmachine/media_type_spec.rb +36 -36
- data/spec/webmachine/request_spec.rb +33 -33
- data/spec/webmachine/resource/authentication_spec.rb +6 -6
- data/spec/webmachine/response_spec.rb +12 -12
- data/spec/webmachine/trace/fsm_spec.rb +8 -8
- data/spec/webmachine/trace/resource_proxy_spec.rb +9 -9
- data/spec/webmachine/trace/trace_store_spec.rb +5 -5
- data/spec/webmachine/trace_spec.rb +3 -3
- data/webmachine.gemspec +2 -6
- metadata +48 -206
- data/lib/webmachine/adapters/hatetepe.rb +0 -108
- data/lib/webmachine/adapters/mongrel.rb +0 -127
- data/lib/webmachine/dispatcher/not_found_resource.rb +0 -5
- data/lib/webmachine/fiber18.rb +0 -88
- data/spec/webmachine/adapters/hatetepe_spec.rb +0 -60
- 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
|
+
```
|