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,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.
|
data/examples/application.rb
CHANGED
@@ -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,
|
28
|
+
app.add_route ['fizz', :buzz, :*], RouteDebugResource
|
29
29
|
# OR add routes this way:
|
30
30
|
app.routes do
|
31
|
-
add [:test, :foo,
|
31
|
+
add [:test, :foo, :*], RouteDebugResource
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
data/examples/debugger.rb
CHANGED
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'
|
data/lib/webmachine/adapter.rb
CHANGED
@@ -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::
|
9
|
-
attr_reader :
|
8
|
+
# @return [Webmachine::Application] returns the application
|
9
|
+
attr_reader :application
|
10
10
|
|
11
|
-
# @
|
12
|
-
|
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(
|
24
|
-
new(
|
17
|
+
def self.run(application)
|
18
|
+
new(application).run
|
25
19
|
end
|
26
20
|
|
27
21
|
# Start the adapter.
|