webmachine 1.2.2 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/CHANGELOG.md +57 -0
- data/Gemfile +20 -15
- data/README.md +89 -91
- data/RELEASING.md +21 -0
- data/Rakefile +5 -21
- data/documentation/adapters.md +41 -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 +224 -0
- data/documentation/how-it-works.md +76 -0
- data/documentation/routes.md +112 -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 +70 -25
- data/lib/webmachine/adapters/rack_mapped.rb +42 -0
- data/lib/webmachine/adapters/reel.rb +22 -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/configuration.rb +1 -1
- data/lib/webmachine/constants.rb +75 -0
- data/lib/webmachine/decision/conneg.rb +12 -10
- data/lib/webmachine/decision/flow.rb +42 -32
- data/lib/webmachine/decision/fsm.rb +14 -21
- data/lib/webmachine/decision/helpers.rb +10 -38
- data/lib/webmachine/dispatcher.rb +13 -10
- data/lib/webmachine/dispatcher/route.rb +45 -9
- data/lib/webmachine/errors.rb +9 -3
- data/lib/webmachine/events.rb +2 -2
- data/lib/webmachine/header_negotiation.rb +25 -0
- data/lib/webmachine/headers.rb +8 -3
- data/lib/webmachine/locale/en.yml +7 -5
- data/lib/webmachine/media_type.rb +10 -8
- data/lib/webmachine/request.rb +67 -26
- data/lib/webmachine/rescueable_exception.rb +62 -0
- data/lib/webmachine/resource.rb +1 -1
- data/lib/webmachine/resource/callbacks.rb +11 -9
- data/lib/webmachine/response.rb +3 -5
- data/lib/webmachine/spec/IO_response.body +1 -0
- data/lib/webmachine/spec/adapter_lint.rb +83 -37
- data/lib/webmachine/spec/test_resource.rb +15 -4
- data/lib/webmachine/streaming/fiber_encoder.rb +1 -5
- data/lib/webmachine/streaming/io_encoder.rb +7 -1
- 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 +17 -9
- data/spec/webmachine/adapter_spec.rb +14 -15
- data/spec/webmachine/adapters/httpkit_spec.rb +10 -0
- data/spec/webmachine/adapters/rack_mapped_spec.rb +71 -0
- data/spec/webmachine/adapters/rack_spec.rb +32 -6
- data/spec/webmachine/adapters/reel_spec.rb +16 -12
- 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 +49 -43
- data/spec/webmachine/decision/falsey_spec.rb +4 -4
- data/spec/webmachine/decision/flow_spec.rb +195 -145
- data/spec/webmachine/decision/fsm_spec.rb +81 -19
- data/spec/webmachine/decision/helpers_spec.rb +20 -20
- data/spec/webmachine/dispatcher/rfc3986_percent_decode_spec.rb +22 -0
- data/spec/webmachine/dispatcher/route_spec.rb +114 -32
- data/spec/webmachine/dispatcher_spec.rb +49 -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 +70 -39
- data/spec/webmachine/rescueable_exception_spec.rb +15 -0
- data/spec/webmachine/resource/authentication_spec.rb +6 -6
- data/spec/webmachine/response_spec.rb +18 -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 +78 -228
- 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,41 @@
|
|
1
|
+
### Adapters
|
2
|
+
|
3
|
+
Webmachine includes adapters for [WEBrick][webrick], [Reel][reel], and
|
4
|
+
[HTTPkit][httpkit]. Additionally, the [Rack][rack] adapter lets it
|
5
|
+
run on any webserver that provides a Rack interface. It also lets it run on
|
6
|
+
[Shotgun][shotgun] ([example][shotgun_example]).
|
7
|
+
|
8
|
+
#### A Note about Rack
|
9
|
+
|
10
|
+
In order to be compatible with popular deployment stacks,
|
11
|
+
Webmachine has a [Rack](https://github.com/rack/rack) adapter (thanks to Jamis Buck).
|
12
|
+
|
13
|
+
Webmachine can be used with Rack middlware features such as Rack::Map and Rack::Cascade as long as any requests/responses that are handled by the Webmachine app are **not** modified by the middleware. The behaviours that are encapsulated in Webmachine assume that no modifications
|
14
|
+
are done to requests or response outside of Webmachine.
|
15
|
+
|
16
|
+
Keep in mind that Webmachine already supports many things that Rack middleware is used for with other HTTP frameworks (eg. etags, specifying supported/preferred Accept and Content-Types).
|
17
|
+
|
18
|
+
The base `Webmachine::Adapters::Rack` class assumes the Webmachine application
|
19
|
+
is mounted at the route path `/` (i.e. not using `Rack::Builder#map` or Rails
|
20
|
+
`ActionDispatch::Routing::Mapper::Base#mount`). In order to
|
21
|
+
map to a subpath, use the `Webmachine::Adapters::RackMapped` adapter instead.
|
22
|
+
|
23
|
+
For an example of using Webmachine with Rack middleware, see the [Pact Broker][middleware-example].
|
24
|
+
|
25
|
+
See the [Rack Adapter API docs][rack-adapter-api-docs] for more information.
|
26
|
+
|
27
|
+
#### A Note about MRI 1.9
|
28
|
+
|
29
|
+
The [Reel][reel] and [HTTPkit][httpkit]
|
30
|
+
adapters might crash with a `SystemStackError` on MRI 1.9 due to its
|
31
|
+
limited fiber stack size. If your application is affected by this, the
|
32
|
+
only known solution is to switch to JRuby, Rubinius or MRI 2.0.
|
33
|
+
|
34
|
+
[webrick]: http://rubydoc.info/stdlib/webrick
|
35
|
+
[reel]: https://github.com/celluloid/reel
|
36
|
+
[httpkit]: https://github.com/lgierth/httpkit
|
37
|
+
[rack]: https://github.com/rack/rack
|
38
|
+
[shotgun]: https://github.com/rtomayko/shotgun
|
39
|
+
[shotgun_example]: https://gist.github.com/4389220
|
40
|
+
[rack-adapter-api-docs]: http://rubydoc.info/gems/webmachine/Webmachine/Adapters/Rack
|
41
|
+
[middleware-example]: https://github.com/bethesque/pact_broker/blob/6dfa71d98e38be94f0776d30bf66cfca58f97d61/lib/pact_broker/app.rb
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Authentication
|
2
|
+
|
3
|
+
To secure a resource, override the `is_authorized?` method to return a boolean indicating whether or not the client is authenticated (ie. your application believes they are who they say they are). Confusingly, the HTTP "401 Unauthorized" response code actually relates to authentication, not authorization (see the [Authorization](#authorization) section below).
|
4
|
+
|
5
|
+
## HTTP Basic Auth
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
|
9
|
+
class MySecureResource < Webmachine::Resource
|
10
|
+
|
11
|
+
include Webmachine::Resource::Authentication
|
12
|
+
|
13
|
+
def is_authorized?(authorization_header)
|
14
|
+
basic_auth(authorization_header, "My Application") do |username, password|
|
15
|
+
@user = User.find_by_username(username)
|
16
|
+
!@user.nil? && @user.auth?(password)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
```
|
23
|
+
|
24
|
+
# Authorization
|
25
|
+
|
26
|
+
Once the client is authenticated (that is, you believe they are who they say they are), override `forbidden?` to return true if the client does not have permission to perform the given method this resource.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
|
30
|
+
class MySecureResource < Webmachine::Resource
|
31
|
+
|
32
|
+
def forbidden?
|
33
|
+
MySecureResourcePolicy.new(@user, my_secure_domain_model).forbidden?(request.method)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
```
|
@@ -0,0 +1,19 @@
|
|
1
|
+
### Application/Configurator
|
2
|
+
|
3
|
+
A call to `Webmachine::Application#configure` returns a `Webmachine::Application` instance,
|
4
|
+
so you could chain other method calls if you like. If you don't want to create your own separate
|
5
|
+
application object `Webmachine.application` will return a global one.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
require 'webmachine'
|
9
|
+
require 'my_resource'
|
10
|
+
|
11
|
+
Webmachine.application.configure do |config|
|
12
|
+
config.ip = '127.0.0.1'
|
13
|
+
config.port = 3000
|
14
|
+
config.adapter = :WEBrick
|
15
|
+
end
|
16
|
+
|
17
|
+
# Start a web server to serve requests via localhost
|
18
|
+
Webmachine.application.run
|
19
|
+
```
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Error handling
|
2
|
+
|
3
|
+
## Handling runtime errors
|
4
|
+
|
5
|
+
Runtime errors should happen infrequently when using Webmachine, as many of the potential causes of "error" will have already been checked in the appropriate callback, and handled with a meaningful HTTP response code (eg. `resource_exists?` or `is_authorized?`).
|
6
|
+
|
7
|
+
To return a custom error response, override `handle_exception` and modify the response body and headers as desired.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
|
11
|
+
class MyResource < Webmachine::Resource
|
12
|
+
|
13
|
+
def handle_exception e
|
14
|
+
response.headers['Content-Type'] = 'application/json'
|
15
|
+
response.body = {:message => e.message, :backtrace => e.backtrace }.to_json
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
```
|
21
|
+
|
22
|
+
Given that this should be a genuine "Server Error", the response code is set to 500, and cannot be overridden in `handle_exception`. If you must set a custom error response code, but were unable to use one of the previous callbacks to set it, use `finish_request` to set the response code as desired.
|
23
|
+
|
24
|
+
## Customising the error response
|
25
|
+
|
26
|
+
You can modify the response headers and body in any callback.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
|
30
|
+
class MyResource < Webmachine::Resource
|
31
|
+
|
32
|
+
def resource_exists?
|
33
|
+
@droid = Droid.find(request.path_info[:droid_id]).tap do | droid |
|
34
|
+
unless droid
|
35
|
+
response.headers['Content-Type'] = "text/plain"
|
36
|
+
response.body = "These aren't the droids you're looking for."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
```
|
44
|
+
|
45
|
+
## Returning a custom error code
|
46
|
+
|
47
|
+
If your response code cannot be determined in an appropriate callback, returning an integer response code from most of the callbacks will cause the response to be returned immediately. You generally only want to do this when new information comes to light, requiring a modification of the response.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
|
51
|
+
class MyResource < Webmachine::Resource
|
52
|
+
|
53
|
+
def content_types_accepted
|
54
|
+
[
|
55
|
+
["application/json", :from_json],
|
56
|
+
["application/xml", :from_xml]
|
57
|
+
]
|
58
|
+
end
|
59
|
+
|
60
|
+
def malformed_request?
|
61
|
+
# Is this JSON or XML? Don't know without a messy if statement.
|
62
|
+
# Maybe cleaner to decide in the response handler for the appropriate Content-Type?
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def from_json
|
67
|
+
return 400 if invalid_json?
|
68
|
+
...
|
69
|
+
end
|
70
|
+
|
71
|
+
def from_xml
|
72
|
+
return 400 if invalid_xml?
|
73
|
+
...
|
74
|
+
end
|
75
|
+
|
76
|
+
def invalid_json?
|
77
|
+
...
|
78
|
+
end
|
79
|
+
|
80
|
+
def invalid_xml?
|
81
|
+
...
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
```
|
@@ -0,0 +1,224 @@
|
|
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`, `process_post`, and `content_types_provided` (if the response has a content type).
|
96
|
+
* Rather than providing a method handler in the `content_type_provided` mappings, put all the code to be executed in `process_post`.
|
97
|
+
* `process_post` must return true, or the HTTP response code.
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class DispatchOrderResource < Webmachine::Resource
|
101
|
+
def content_types_provided
|
102
|
+
[["application/json"]]
|
103
|
+
end
|
104
|
+
|
105
|
+
def allowed_methods
|
106
|
+
["POST"]
|
107
|
+
end
|
108
|
+
|
109
|
+
def resource_exists?
|
110
|
+
@order = Order.find(id)
|
111
|
+
end
|
112
|
+
|
113
|
+
def process_post
|
114
|
+
@order.dispatch(params['some_param'])
|
115
|
+
response.body = { message: "Successfully dispatched order #{id}" }.to_json
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def id
|
122
|
+
request.path_info[:id]
|
123
|
+
end
|
124
|
+
|
125
|
+
def params
|
126
|
+
JSON.parse(request.body.to_s)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
```
|
131
|
+
|
132
|
+
# PUT
|
133
|
+
* Override `resource_exists?`, `content_types_accepted`, `allowed_methods`, and implement the method to create/replace the resource.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
class OrderResource < Webmachine::Resource
|
137
|
+
|
138
|
+
def allowed_methods
|
139
|
+
["PUT"]
|
140
|
+
end
|
141
|
+
|
142
|
+
def content_types_accepted
|
143
|
+
[["application/json", :from_json]]
|
144
|
+
end
|
145
|
+
|
146
|
+
# Note that returning falsey will NOT result in a 404 for PUT requests.
|
147
|
+
# See note below.
|
148
|
+
def resource_exists?
|
149
|
+
order
|
150
|
+
end
|
151
|
+
|
152
|
+
def from_json
|
153
|
+
# Remember PUT should replace the entire resource, not merge the attributes! That's what PATCH is for.
|
154
|
+
# It's also why you should not expose your database IDs as your API IDs.
|
155
|
+
order.destroy if order
|
156
|
+
new_order = Order.new(params)
|
157
|
+
new_order.save(id)
|
158
|
+
response.body = new_order.to_json
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def order
|
164
|
+
@order ||= Order.find(id)
|
165
|
+
end
|
166
|
+
|
167
|
+
def params
|
168
|
+
JSON.parse(request.body.to_s)
|
169
|
+
end
|
170
|
+
|
171
|
+
def id
|
172
|
+
request.path_info[:id]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
If you wish to disallow PUT to a non-existent resource, read more [here](https://github.com/webmachine/webmachine-ruby/issues/207#issuecomment-132604379).
|
178
|
+
|
179
|
+
# PATCH
|
180
|
+
* Webmachine does not currently support PATCH requests. See https://github.com/webmachine/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.
|
181
|
+
|
182
|
+
# DELETE
|
183
|
+
* Override `resource_exists?` and `delete_resource`
|
184
|
+
* `delete_resource` must return true
|
185
|
+
* See callbacks.rb for documentation on asynchronous deletes.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
class OrderResource < Webmachine::Resource
|
189
|
+
|
190
|
+
def allowed_methods
|
191
|
+
["DELETE"]
|
192
|
+
end
|
193
|
+
|
194
|
+
def resource_exists?
|
195
|
+
order
|
196
|
+
end
|
197
|
+
|
198
|
+
def delete_resource
|
199
|
+
order.destroy
|
200
|
+
true
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
def order
|
206
|
+
@order ||= Order.find(id)
|
207
|
+
end
|
208
|
+
|
209
|
+
def id
|
210
|
+
request.path_info[:id]
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
Thanks to [oestrich][oestrich] for putting together the original example. You can see the full source code [here][source].
|
217
|
+
|
218
|
+
[oestrich]: https://github.com/oestrich
|
219
|
+
[source]: https://gist.github.com/oestrich/3638605
|
220
|
+
|
221
|
+
<a name="callback-order"></a>
|
222
|
+
## What order are the callbacks invoked in?
|
223
|
+
|
224
|
+
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]: https://webmachine.github.io/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
|