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,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