acceptable_api 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.rvmrc +48 -0
- data/README.md +114 -46
- data/Rakefile +11 -1
- data/acceptable_api.gemspec +6 -2
- data/example/Gemfile +1 -7
- data/example/config.ru +40 -1
- data/example/example.rb +19 -33
- data/lib/acceptable_api.rb +14 -151
- data/lib/acceptable_api/accepts.rb +16 -0
- data/lib/acceptable_api/action.rb +43 -0
- data/lib/acceptable_api/application.rb +26 -0
- data/lib/acceptable_api/builder.rb +31 -0
- data/lib/acceptable_api/controller.rb +18 -0
- data/lib/acceptable_api/mapper.rb +31 -0
- data/lib/acceptable_api/mappers.rb +36 -0
- data/lib/acceptable_api/missing_controller.rb +8 -0
- data/lib/acceptable_api/missing_mapper.rb +9 -0
- data/lib/acceptable_api/missing_route.rb +9 -0
- data/lib/acceptable_api/route.rb +50 -0
- data/lib/acceptable_api/routes.rb +20 -0
- data/lib/acceptable_api/version.rb +1 -1
- data/test/acceptance/request_different_mime_type_versions_test.rb +15 -0
- data/test/acceptance/request_unknown_mime_type_test.rb +29 -0
- data/test/acceptance/test_helper.rb +47 -0
- data/test/test_helper.rb +9 -0
- metadata +111 -16
data/.rvmrc
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# This is an RVM Project .rvmrc file, used to automatically load the ruby
|
|
4
|
+
# development environment upon cd'ing into the directory
|
|
5
|
+
|
|
6
|
+
# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
|
|
7
|
+
# Only full ruby name is supported here, for short names use:
|
|
8
|
+
# echo "rvm use 1.9.3" > .rvmrc
|
|
9
|
+
environment_id="ruby-1.9.3-p194@acceptable_api"
|
|
10
|
+
|
|
11
|
+
# Uncomment the following lines if you want to verify rvm version per project
|
|
12
|
+
# rvmrc_rvm_version="1.14.5 (stable)" # 1.10.1 seams as a safe start
|
|
13
|
+
# eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
|
|
14
|
+
# echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
|
|
15
|
+
# return 1
|
|
16
|
+
# }
|
|
17
|
+
|
|
18
|
+
# First we attempt to load the desired environment directly from the environment
|
|
19
|
+
# file. This is very fast and efficient compared to running through the entire
|
|
20
|
+
# CLI and selector. If you want feedback on which environment was used then
|
|
21
|
+
# insert the word 'use' after --create as this triggers verbose mode.
|
|
22
|
+
if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
|
|
23
|
+
&& -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
|
|
24
|
+
then
|
|
25
|
+
\. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
|
|
26
|
+
[[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
|
|
27
|
+
\. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
|
|
28
|
+
else
|
|
29
|
+
# If the environment file has not yet been created, use the RVM CLI to select.
|
|
30
|
+
rvm --create "$environment_id" || {
|
|
31
|
+
echo "Failed to create RVM environment '${environment_id}'."
|
|
32
|
+
return 1
|
|
33
|
+
}
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# If you use bundler, this might be useful to you:
|
|
37
|
+
# if [[ -s Gemfile ]] && {
|
|
38
|
+
# ! builtin command -v bundle >/dev/null ||
|
|
39
|
+
# builtin command -v bundle | GREP_OPTIONS= \grep $rvm_path/bin/bundle >/dev/null
|
|
40
|
+
# }
|
|
41
|
+
# then
|
|
42
|
+
# printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
|
|
43
|
+
# gem install bundler
|
|
44
|
+
# fi
|
|
45
|
+
# if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
|
|
46
|
+
# then
|
|
47
|
+
# bundle install | GREP_OPTIONS= \grep -vE '^Using|Your bundle is complete'
|
|
48
|
+
# fi
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Build an acceptable API.
|
|
4
4
|
|
|
5
|
-
HTTP is pretty darn awesome you guys. Part of HTTP -
|
|
5
|
+
HTTP is pretty darn awesome you guys. Part of HTTP - `Accept` headers -
|
|
6
6
|
allows the clients of our API to tell us what representation they want to work
|
|
7
7
|
with. We should probably pay attention to them, hey?
|
|
8
8
|
|
|
@@ -33,13 +33,22 @@ Or install it yourself as:
|
|
|
33
33
|
|
|
34
34
|
$ gem install acceptable_api
|
|
35
35
|
|
|
36
|
+
|
|
36
37
|
## Usage
|
|
37
38
|
|
|
38
|
-
Assume you have a class that you want to expose via a lovely HTTP API
|
|
39
|
+
Assume you have a class that you want to expose via a lovely HTTP ReST API:
|
|
39
40
|
|
|
41
|
+
# app/models/sandwich.rb
|
|
40
42
|
class Sandwich
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
attr_accessor :id
|
|
44
|
+
private :id=
|
|
45
|
+
|
|
46
|
+
def initialize id
|
|
47
|
+
self.id = id
|
|
48
|
+
self.bread = "Brown"
|
|
49
|
+
self.fillings = %w(mayo chicken salad)
|
|
50
|
+
self.name = "Chicken Mayo Salad"
|
|
51
|
+
self.made_at = Time.now
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
attr_accessor :fillings
|
|
@@ -48,35 +57,38 @@ Assume you have a class that you want to expose via a lovely HTTP API.
|
|
|
48
57
|
attr_accessor :made_at
|
|
49
58
|
end
|
|
50
59
|
|
|
51
|
-
You'd declare that you wanted to expose it via the API
|
|
52
|
-
|
|
60
|
+
You'd declare that you wanted to expose it via the API like this:
|
|
61
|
+
|
|
62
|
+
# app/resources/sandwich_resource.rb
|
|
63
|
+
class SandwichResource
|
|
64
|
+
include AcceptableApi::Controller
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
def show_sandwich
|
|
67
|
+
# Normally this would be a database lookup but since this is just an
|
|
68
|
+
# example I create a new instance to keep things simple
|
|
69
|
+
Sandwich.new params[:id]
|
|
57
70
|
end
|
|
58
71
|
end
|
|
59
72
|
|
|
60
|
-
Of course, this needs to be run somehow.
|
|
61
|
-
|
|
73
|
+
Of course, this needs to be run somehow. I've chosen to do this via Rack. In
|
|
74
|
+
`config.ru` do this:
|
|
62
75
|
|
|
63
76
|
require 'acceptable_api'
|
|
64
|
-
require '
|
|
77
|
+
require 'app/models/sandwich'
|
|
78
|
+
require 'app/resource/sandwich_resource'
|
|
65
79
|
|
|
66
|
-
|
|
80
|
+
app = AcceptableApi::Builder.new
|
|
81
|
+
app.expose 'SandwichResource#show_sandwich', at: '/sandwiches/:id',
|
|
82
|
+
via: 'get'
|
|
83
|
+
run app.to_app
|
|
67
84
|
|
|
68
85
|
You can now use `rackup` as normal to launch a web server, and `curl` to access
|
|
69
|
-
your API:
|
|
86
|
+
your API, requesting a plain text representation of sandwich 123:
|
|
70
87
|
|
|
71
|
-
$ curl -i http://localhost:9292/sandwiches/123
|
|
88
|
+
$ curl -H 'Accept: application/json' -i http://localhost:9292/sandwiches/123
|
|
72
89
|
HTTP/1.1 406 Not Acceptable
|
|
73
|
-
X-Frame-Options: sameorigin
|
|
74
|
-
X-Xss-Protection: 1; mode=block
|
|
75
90
|
Content-Type: application/javascript
|
|
76
91
|
Content-Length: 21
|
|
77
|
-
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
|
|
78
|
-
Date: Sat, 21 Apr 2012 19:57:59 GMT
|
|
79
|
-
Connection: Keep-Alive
|
|
80
92
|
|
|
81
93
|
{
|
|
82
94
|
"links": [
|
|
@@ -85,53 +97,45 @@ your API:
|
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
We got a `406 Not Acceptable` response because AcceptableApi doesn't know how to
|
|
88
|
-
respond with
|
|
89
|
-
told it how to respond
|
|
100
|
+
respond with an `application/json` representation of a Sandwich. That's fair: we
|
|
101
|
+
haven't told it how to respond with /any/ representations of sandwiches yet. Do
|
|
102
|
+
it like this in `config.ru`, before calling `#to_app`:
|
|
90
103
|
|
|
91
|
-
|
|
104
|
+
app.register Sandwich => 'application/json' do |sandwich|
|
|
92
105
|
JSON.generate :id => sandwich.id
|
|
93
106
|
end
|
|
94
107
|
|
|
95
108
|
Let's try requesting the resource again:
|
|
96
109
|
|
|
97
|
-
$ curl -i http://localhost:
|
|
110
|
+
$ curl -H 'Accept: application/json' -i http://localhost:9292/sandwiches/123
|
|
98
111
|
HTTP/1.1 200 OK
|
|
99
|
-
X-Frame-Options: sameorigin
|
|
100
|
-
X-Xss-Protection: 1; mode=block
|
|
101
112
|
Content-Type: application/json
|
|
102
113
|
Content-Length: 12
|
|
103
|
-
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
|
|
104
|
-
Date: Sat, 21 Apr 2012 20:03:14 GMT
|
|
105
|
-
Connection: Keep-Alive
|
|
106
114
|
|
|
107
115
|
{"id":"123"}
|
|
108
116
|
|
|
109
|
-
Ace, we got a response
|
|
117
|
+
Ace, we got a response, and it's the JSON represenation of the sandwich. What
|
|
118
|
+
happens if we ask for a plain text representation?
|
|
110
119
|
|
|
111
120
|
$ curl -H 'Accept: text/plain' -i http://localhost:9292/sandwiches/123
|
|
112
121
|
HTTP/1.1 406 Not Acceptable
|
|
113
|
-
X-Frame-Options: sameorigin
|
|
114
|
-
X-Xss-Protection: 1; mode=block
|
|
115
122
|
Content-Type: application/javascript
|
|
116
123
|
Content-Length: 146
|
|
117
|
-
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
|
|
118
|
-
Date: Sat, 21 Apr 2012 20:04:46 GMT
|
|
119
|
-
Connection: Keep-Alive
|
|
120
124
|
|
|
121
125
|
{
|
|
122
126
|
"links": [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
{
|
|
128
|
+
"rel": "alternative",
|
|
129
|
+
"type": "application/json",
|
|
130
|
+
"uri": "http://localhost:9292/sandwiches/123"
|
|
131
|
+
}
|
|
128
132
|
]
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
As expected, this is a `406 Not Acceptable` response, but we take the
|
|
132
136
|
opportunity to provide a list of alternative representations that the client may
|
|
133
|
-
want to check out.
|
|
134
|
-
request should the client want to do so.
|
|
137
|
+
want to check out. The `application/json` representation is listed with the type
|
|
138
|
+
and the URI to request should the client want to do so.
|
|
135
139
|
|
|
136
140
|
Time passes, and a we decide that our API would be more useful if it returned
|
|
137
141
|
the fillings and bread used in the sandwich, and we want to replace the database
|
|
@@ -150,18 +154,31 @@ which specifies the returned document:
|
|
|
150
154
|
|
|
151
155
|
And we register the type with AcceptableApi:
|
|
152
156
|
|
|
153
|
-
|
|
157
|
+
app.register Sandwich => 'application/vnd.acme.sandwich-v1+json' do |sandwich|
|
|
154
158
|
JSON.generate :name => sandwich.name, :fillings => sandwich.fillings,
|
|
155
159
|
:bread => sandwich.bread
|
|
156
160
|
end
|
|
157
161
|
|
|
158
162
|
And we make the request:
|
|
159
163
|
|
|
160
|
-
|
|
164
|
+
$ curl -H 'Accept: application/vnd.acme.sandwich-v1+json' -i http://localhost:9292/sandwiches/123
|
|
165
|
+
HTTP/1.1 200 OK
|
|
166
|
+
Content-Type: application/vnd.acme.sandwich-v1+json
|
|
167
|
+
Content-Length: 75
|
|
168
|
+
|
|
169
|
+
{"name":"Bleaugh","fillings":["jam","avacado","anchovies"],"bread":"brown"}
|
|
170
|
+
|
|
171
|
+
Making a request for the normal `application/json` representation still works:
|
|
172
|
+
|
|
173
|
+
$ curl -H 'Accept: application/json' -i http://localhost:9292/sandwiches/123
|
|
174
|
+
HTTP/1.1 200 OK
|
|
175
|
+
Content-Type: application/json
|
|
176
|
+
Content-Length: 12
|
|
177
|
+
|
|
178
|
+
{"id":"123"}
|
|
161
179
|
|
|
162
|
-
See `example
|
|
180
|
+
See the example directory, `example/`, for a working example.
|
|
163
181
|
|
|
164
|
-
TODO: Write usage instructions here
|
|
165
182
|
|
|
166
183
|
## Contributing
|
|
167
184
|
|
|
@@ -170,3 +187,54 @@ TODO: Write usage instructions here
|
|
|
170
187
|
3. Commit your changes (`git commit -am 'Added some feature'`)
|
|
171
188
|
4. Push to the branch (`git push origin my-new-feature`)
|
|
172
189
|
5. Create new Pull Request
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## TODO
|
|
193
|
+
|
|
194
|
+
* Still need to add tests and discover how to work with the other HTTP verbs,
|
|
195
|
+
starting with POST, PUT, DELETE and OPTIONS. HEAD could be handy as well but
|
|
196
|
+
it's quite possible that software further up the stack could just strip out
|
|
197
|
+
the GET entity to create a valid HEAD response. As a first scratch I'm
|
|
198
|
+
imagining changing the #expose call to something like:
|
|
199
|
+
|
|
200
|
+
app.expose SandwichResource, at: '/sandwiches/:id',
|
|
201
|
+
get: 'show', put: 'update', delete: 'destroy'
|
|
202
|
+
|
|
203
|
+
* I don't like defining the conversions in`config.ru`. It would be lovely if
|
|
204
|
+
these could be picked up automatically on start-up - but I'd settle for
|
|
205
|
+
something less verbose that I currently have. Possibly a slight adaptation of
|
|
206
|
+
the expose call:
|
|
207
|
+
|
|
208
|
+
app.expose Sandwich, at: '/sandwiches/:id',
|
|
209
|
+
get: 'show', put: 'update', delete: 'destroy'
|
|
210
|
+
|
|
211
|
+
This could guess we want the use `SandwichResource` as the controller, and it
|
|
212
|
+
could examine `app/views/sandwiches/**/*.rb` for convertors:
|
|
213
|
+
|
|
214
|
+
convert Sandwich => 'application/vnd.acme.sandwich-v1+xml' do |sandwich|
|
|
215
|
+
xml = Builder::XmlMarkup.new
|
|
216
|
+
xml.sandwich do |s|
|
|
217
|
+
s.name sandwich.name
|
|
218
|
+
s.bread sandwich.bread
|
|
219
|
+
s.fillings do |f|
|
|
220
|
+
sandwich.fillings.each do |filling|
|
|
221
|
+
f.filling filling
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
* Need to work out how I should deal with authentication etc. Want to keep
|
|
228
|
+
this clean. A controller should be able to return any of the HTTP statuses
|
|
229
|
+
that makes sense, including 401 / 403.
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
## Authors
|
|
233
|
+
|
|
234
|
+
Craig R Webster <craig@barkingiguana.com>
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
## Licence
|
|
238
|
+
|
|
239
|
+
Released under the terms of the MIT licence, a copy of which can be found in the
|
|
240
|
+
`LICENCE` file distributed with this project.
|
data/Rakefile
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env rake
|
|
2
|
-
require
|
|
2
|
+
require 'bundler/gem_tasks'
|
|
3
|
+
require 'rake/testtask'
|
|
4
|
+
|
|
5
|
+
Rake::TestTask.new do |t|
|
|
6
|
+
t.libs << "test"
|
|
7
|
+
t.test_files = FileList['test/**/*_test.rb']
|
|
8
|
+
t.verbose = true
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "Run tests"
|
|
12
|
+
task :default => :test
|
data/acceptable_api.gemspec
CHANGED
|
@@ -4,7 +4,7 @@ require File.expand_path('../lib/acceptable_api/version', __FILE__)
|
|
|
4
4
|
Gem::Specification.new do |gem|
|
|
5
5
|
gem.authors = ["Craig R Webster"]
|
|
6
6
|
gem.email = ["craig@barkingiguana.com"]
|
|
7
|
-
gem.description = %q{HTTP lets clients
|
|
7
|
+
gem.description = %q{HTTP lets clients send Accept headers. We should probably use that to work out what they'll accept as a response, yea?}
|
|
8
8
|
gem.summary = %q{Build an Acceptable API}
|
|
9
9
|
gem.homepage = "http://barkingiguana.com/2011/12/05/principles-of-service-design-program-to-an-interface/"
|
|
10
10
|
|
|
@@ -14,8 +14,12 @@ Gem::Specification.new do |gem|
|
|
|
14
14
|
gem.name = "acceptable_api"
|
|
15
15
|
gem.require_paths = ["lib"]
|
|
16
16
|
gem.version = AcceptableApi::VERSION
|
|
17
|
+
gem.add_runtime_dependency 'json'
|
|
17
18
|
gem.add_runtime_dependency 'rack'
|
|
18
19
|
gem.add_runtime_dependency 'rack-accept'
|
|
19
20
|
gem.add_runtime_dependency 'rack-accept-header-updater'
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
gem.add_development_dependency 'rack-test'
|
|
23
|
+
gem.add_development_dependency 'test-unit'
|
|
24
|
+
gem.add_development_dependency 'rake'
|
|
21
25
|
end
|
data/example/Gemfile
CHANGED
data/example/config.ru
CHANGED
|
@@ -1,4 +1,43 @@
|
|
|
1
1
|
require 'acceptable_api'
|
|
2
2
|
require './example'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'builder'
|
|
6
|
+
|
|
7
|
+
app = AcceptableApi::Builder.new
|
|
8
|
+
|
|
9
|
+
app.register Sandwich => 'text/plain' do |sandwich|
|
|
10
|
+
s = []
|
|
11
|
+
s << sandwich.name
|
|
12
|
+
s << sandwich.fillings.sort.join(',')
|
|
13
|
+
s << sandwich.bread
|
|
14
|
+
s << sandwich.made_at.iso8601
|
|
15
|
+
s.join "\n"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
app.register Sandwich => 'application/vnd.acme.sandwich-v1+xml' do |sandwich|
|
|
19
|
+
xml = Builder::XmlMarkup.new
|
|
20
|
+
xml.sandwich do |s|
|
|
21
|
+
s.name sandwich.name
|
|
22
|
+
s.bread sandwich.bread
|
|
23
|
+
s.fillings do |f|
|
|
24
|
+
sandwich.fillings.each do |filling|
|
|
25
|
+
f.filling filling
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
app.register Sandwich => 'application/json' do |sandwich|
|
|
32
|
+
JSON.generate :id => sandwich.id
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
app.register Sandwich => 'application/vnd.acme.sandwich-v1+json' do |sandwich|
|
|
36
|
+
JSON.generate :name => sandwich.name, :fillings => sandwich.fillings,
|
|
37
|
+
:bread => sandwich.bread
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
app.expose 'SandwichResource#show_sandwich', at: '/sandwiches/:id',
|
|
41
|
+
via: 'get'
|
|
42
|
+
|
|
43
|
+
run app.to_app
|
data/example/example.rb
CHANGED
|
@@ -1,41 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
class Sandwich
|
|
2
|
+
attr_accessor :id
|
|
3
|
+
private :id=
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
:made_at => Time.now, :id => id, :name => "Bleaugh"
|
|
5
|
+
def initialize id
|
|
6
|
+
self.id = id
|
|
7
|
+
self.bread = "Brown"
|
|
8
|
+
self.fillings = %w(mayo chicken salad)
|
|
9
|
+
self.name = "Chicken Mayo Salad"
|
|
10
|
+
self.made_at = Time.now
|
|
12
11
|
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
AcceptableApi.register Sandwich, 'application/json' do |sandwich, request|
|
|
16
|
-
JSON.generate :id => sandwich.id
|
|
17
|
-
end
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
attr_accessor :fillings
|
|
14
|
+
attr_accessor :bread
|
|
15
|
+
attr_accessor :name
|
|
16
|
+
attr_accessor :made_at
|
|
22
17
|
end
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
xml.sandwich do |s|
|
|
27
|
-
s.name sandwich.name
|
|
28
|
-
s.bread sandwich.bread
|
|
29
|
-
s.fillings do |f|
|
|
30
|
-
sandwich.fillings.each do |filling|
|
|
31
|
-
f.filling filling
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
19
|
+
class SandwichResource
|
|
20
|
+
include AcceptableApi::Controller
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
def show_sandwich
|
|
23
|
+
# Normally this would be a database lookup but since this is just an
|
|
24
|
+
# example I create a new instance to keep things simple
|
|
25
|
+
Sandwich.new params[:id]
|
|
40
26
|
end
|
|
41
27
|
end
|
data/lib/acceptable_api.rb
CHANGED
|
@@ -1,158 +1,21 @@
|
|
|
1
|
-
require 'sinatra'
|
|
2
1
|
require 'singleton'
|
|
2
|
+
require 'json'
|
|
3
3
|
require 'rack/accept'
|
|
4
4
|
require 'rack/accept_header_updater'
|
|
5
5
|
|
|
6
|
-
require
|
|
6
|
+
require 'acceptable_api/accepts.rb'
|
|
7
|
+
require 'acceptable_api/action.rb'
|
|
8
|
+
require 'acceptable_api/application.rb'
|
|
9
|
+
require 'acceptable_api/builder.rb'
|
|
10
|
+
require 'acceptable_api/controller.rb'
|
|
11
|
+
require 'acceptable_api/mapper.rb'
|
|
12
|
+
require 'acceptable_api/mappers.rb'
|
|
13
|
+
require 'acceptable_api/missing_controller.rb'
|
|
14
|
+
require 'acceptable_api/missing_mapper.rb'
|
|
15
|
+
require 'acceptable_api/missing_route.rb'
|
|
16
|
+
require 'acceptable_api/route.rb'
|
|
17
|
+
require 'acceptable_api/routes.rb'
|
|
18
|
+
require 'acceptable_api/version.rb'
|
|
7
19
|
|
|
8
20
|
module AcceptableApi
|
|
9
|
-
def self.register klass, mime_type, &map_block
|
|
10
|
-
Mapper.register klass, mime_type, &map_block
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
class MissingMapper
|
|
14
|
-
include Singleton
|
|
15
|
-
|
|
16
|
-
def mime_type
|
|
17
|
-
'application/javascript'
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def execute resource, request
|
|
21
|
-
# set the response to "no acceptable representation available"
|
|
22
|
-
mappers = Mapper.for resource.class
|
|
23
|
-
body = {
|
|
24
|
-
:links => mappers.mime_types.map do |mime_type|
|
|
25
|
-
{ :rel => "alternative", :type => mime_type, :uri => request.uri }
|
|
26
|
-
end
|
|
27
|
-
}
|
|
28
|
-
[ 406, { 'Content-Type' => 'application/javascript' }, JSON.pretty_generate(body) ]
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
class Mappers
|
|
33
|
-
attr_accessor :mappers
|
|
34
|
-
private :mappers=, :mappers
|
|
35
|
-
|
|
36
|
-
def initialize mappers
|
|
37
|
-
self.mappers = mappers
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def to accepts
|
|
41
|
-
acceptable_mime_types = accepts.order mime_types
|
|
42
|
-
acceptable_mappers = acceptable_mime_types.map { |mt|
|
|
43
|
-
mappers.detect { |m| m.mime_type == mt }
|
|
44
|
-
}
|
|
45
|
-
return acceptable_mappers if acceptable_mappers.any?
|
|
46
|
-
[ MissingMapper.instance ]
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def mime_types
|
|
50
|
-
mappers.map { |m| m.mime_type }.sort.uniq
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
class Mapper
|
|
55
|
-
[ :klass, :mime_type, :map_block ].each do |a|
|
|
56
|
-
attr_accessor a
|
|
57
|
-
private "#{a}="
|
|
58
|
-
end
|
|
59
|
-
private :map_block
|
|
60
|
-
|
|
61
|
-
def initialize klass, mime_type, &map_block
|
|
62
|
-
self.klass = klass
|
|
63
|
-
self.mime_type = mime_type
|
|
64
|
-
self.map_block = map_block
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def execute resource, request
|
|
68
|
-
body = map_block.call resource, request
|
|
69
|
-
[ status, headers, body ]
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def status
|
|
73
|
-
200
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def headers
|
|
77
|
-
{ 'Content-Type' => mime_type }
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def self.for klass
|
|
81
|
-
klass_mappers = mappers.select { |m| m.klass == klass }
|
|
82
|
-
Mappers.new klass_mappers
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
class << self;
|
|
86
|
-
attr_accessor :mappers
|
|
87
|
-
protected :mappers=, :mappers
|
|
88
|
-
end
|
|
89
|
-
self.mappers = []
|
|
90
|
-
|
|
91
|
-
def self.register klass, mime_type, &map_block
|
|
92
|
-
mapper = Mapper.new klass, mime_type, &map_block
|
|
93
|
-
self.mappers << mapper
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# This needs to mimic as much of the Sinatra API as we use.
|
|
98
|
-
# Probably we want to set headers and content_type, not sure what else.
|
|
99
|
-
class Response
|
|
100
|
-
attr_accessor :params
|
|
101
|
-
def initialize options = {}
|
|
102
|
-
self.params = options[:params]
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
class Accepts
|
|
107
|
-
attr_accessor :request
|
|
108
|
-
private :request=, :request
|
|
109
|
-
|
|
110
|
-
def initialize env
|
|
111
|
-
self.request = Rack::Accept::Request.new env
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def order mime_types
|
|
115
|
-
ordered = request.media_type.sort_with_qvalues mime_types, false
|
|
116
|
-
ordered.map! { |q, mt| mt }
|
|
117
|
-
ordered
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
class Request
|
|
122
|
-
attr_accessor :request
|
|
123
|
-
private :request=, :request
|
|
124
|
-
|
|
125
|
-
def initialize rack_request
|
|
126
|
-
self.request = rack_request
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def respond_with resource
|
|
130
|
-
accepts = Accepts.new request.env
|
|
131
|
-
mappers = Mapper.for(resource.class).to(accepts)
|
|
132
|
-
mapper = mappers[0]
|
|
133
|
-
code, headers, body = mapper.execute resource, self
|
|
134
|
-
headers["Content-Length"] = body.bytesize.to_s
|
|
135
|
-
[ code, headers, body ]
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def uri
|
|
139
|
-
request.url
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
class Controller < Sinatra::Base
|
|
144
|
-
use Rack::AcceptHeaderUpdater
|
|
145
|
-
|
|
146
|
-
def self.get path, &block
|
|
147
|
-
super path do
|
|
148
|
-
response = Response.new :params => params
|
|
149
|
-
resource = response.instance_eval &block
|
|
150
|
-
api = Request.new request
|
|
151
|
-
s, h, body = api.respond_with resource
|
|
152
|
-
status s
|
|
153
|
-
headers h
|
|
154
|
-
body
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
21
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module AcceptableApi
|
|
2
|
+
class Accepts
|
|
3
|
+
attr_accessor :request
|
|
4
|
+
private :request=, :request
|
|
5
|
+
|
|
6
|
+
def initialize env
|
|
7
|
+
self.request = Rack::Accept::Request.new env
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def order mime_types
|
|
11
|
+
ordered = request.media_type.sort_with_qvalues mime_types, false
|
|
12
|
+
ordered.map! { |q, mt| mt }
|
|
13
|
+
ordered
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module AcceptableApi
|
|
2
|
+
class Action
|
|
3
|
+
attr_accessor :route
|
|
4
|
+
private :route=, :route
|
|
5
|
+
|
|
6
|
+
attr_accessor :request
|
|
7
|
+
private :request=, :request
|
|
8
|
+
|
|
9
|
+
attr_accessor :mappers
|
|
10
|
+
private :mappers=, :mappers
|
|
11
|
+
|
|
12
|
+
def initialize route, request, mappers
|
|
13
|
+
self.route = route
|
|
14
|
+
self.request = request
|
|
15
|
+
self.mappers = mappers
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def execute
|
|
19
|
+
resource = controller.perform_action
|
|
20
|
+
resource_mappers = mappers.from resource.class
|
|
21
|
+
mapper = resource_mappers.to acceptable_mime_types
|
|
22
|
+
if mapper.missing?
|
|
23
|
+
body = {
|
|
24
|
+
:links => resource_mappers.mime_types.map do |mime_type|
|
|
25
|
+
{ :rel => "alternative", 'Content-Type' => mime_type, :uri => request.url, :method => request.request_method }
|
|
26
|
+
end
|
|
27
|
+
}
|
|
28
|
+
[ 406, { 'Content-Type' => 'application/json' }, [ JSON.pretty_generate(body) ] ]
|
|
29
|
+
else
|
|
30
|
+
body = mapper.execute resource, request
|
|
31
|
+
[ 200, { 'Content-Type' => mapper.to }, [ body ] ]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def controller
|
|
36
|
+
route.controller_for_request request
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def acceptable_mime_types
|
|
40
|
+
Accepts.new request.env
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module AcceptableApi
|
|
2
|
+
class Application
|
|
3
|
+
attr_accessor :routes
|
|
4
|
+
protected :routes=, :routes
|
|
5
|
+
|
|
6
|
+
attr_accessor :mappers
|
|
7
|
+
protected :mappers=, :mappers
|
|
8
|
+
|
|
9
|
+
def initialize mappers, routes
|
|
10
|
+
self.mappers = mappers
|
|
11
|
+
self.routes = routes
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call env
|
|
15
|
+
request = Rack::Request.new env
|
|
16
|
+
action = action_for request
|
|
17
|
+
action.execute
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def action_for request
|
|
21
|
+
route = routes.for request
|
|
22
|
+
Action.new route, request, mappers
|
|
23
|
+
end
|
|
24
|
+
private :action_for
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module AcceptableApi
|
|
2
|
+
class Builder
|
|
3
|
+
attr_accessor :mappers
|
|
4
|
+
protected :mappers=, :mappers
|
|
5
|
+
|
|
6
|
+
attr_accessor :routes
|
|
7
|
+
protected :routes=, :routes
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
self.mappers = Mappers.new
|
|
11
|
+
self.routes = Routes.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def register from_to, &map_block
|
|
15
|
+
from = from_to.keys[0]
|
|
16
|
+
to = from_to.values[0]
|
|
17
|
+
mapper = Mapper.new from, to, &map_block
|
|
18
|
+
self.mappers << mapper
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def expose controller_action, options = {}
|
|
22
|
+
route = Route.new options, controller_action
|
|
23
|
+
self.routes << route
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_app
|
|
27
|
+
Application.new mappers, routes
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module AcceptableApi
|
|
2
|
+
module Controller
|
|
3
|
+
attr_accessor :params
|
|
4
|
+
protected :params, :params=
|
|
5
|
+
|
|
6
|
+
attr_accessor :action
|
|
7
|
+
protected :action, :action=
|
|
8
|
+
|
|
9
|
+
def initialize params, action
|
|
10
|
+
self.params = params
|
|
11
|
+
self.action = action
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def perform_action
|
|
15
|
+
send action
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module AcceptableApi
|
|
2
|
+
class Mapper
|
|
3
|
+
[ :from, :to, :map_block ].each do |a|
|
|
4
|
+
attr_accessor a
|
|
5
|
+
private "#{a}="
|
|
6
|
+
end
|
|
7
|
+
private :map_block
|
|
8
|
+
|
|
9
|
+
def missing?
|
|
10
|
+
false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize from, to, &map_block
|
|
14
|
+
self.from = from
|
|
15
|
+
self.to = to
|
|
16
|
+
self.map_block = map_block
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def from? desired
|
|
20
|
+
from == desired
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to? desired
|
|
24
|
+
to == desired
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def execute resource, request
|
|
28
|
+
map_block.call resource, request
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module AcceptableApi
|
|
2
|
+
class Mappers
|
|
3
|
+
attr_accessor :mappers
|
|
4
|
+
private :mappers=, :mappers
|
|
5
|
+
|
|
6
|
+
def initialize mappers = []
|
|
7
|
+
self.mappers = mappers
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def << mapper
|
|
11
|
+
mappers << mapper
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def from what
|
|
15
|
+
Mappers.new mappers.select { |m| m.from? what }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to accepts
|
|
19
|
+
acceptable_mime_types = accepts.order mime_types
|
|
20
|
+
mapper = acceptable_mime_types.map { |mt|
|
|
21
|
+
mappers.detect { |m| m.to? mt }
|
|
22
|
+
}[0]
|
|
23
|
+
return mapper unless mapper.nil?
|
|
24
|
+
MissingMapper.instance
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def mime_types
|
|
28
|
+
mappers.map { |m| m.to }.sort.uniq
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def each &block
|
|
32
|
+
mappers.each &block
|
|
33
|
+
end
|
|
34
|
+
include Enumerable
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module AcceptableApi
|
|
2
|
+
class Route
|
|
3
|
+
[ :controller_action, :constraints ].each do |a|
|
|
4
|
+
attr_accessor a
|
|
5
|
+
private "#{a}=", a
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize constraints, controller_action
|
|
9
|
+
self.constraints = constraints
|
|
10
|
+
self.controller_action = controller_action
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def match? request
|
|
14
|
+
return false unless constraints[:via].upcase == request.request_method
|
|
15
|
+
return false unless path_regex.match request.path
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def path_regex
|
|
20
|
+
named_captures = constraints[:at].gsub /\:([^\/]+)/, '(?<\1>[^\/]+)'
|
|
21
|
+
Regexp.new "^#{named_captures}$"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def params_from request
|
|
25
|
+
matches = path_regex.match request.path
|
|
26
|
+
matches.names.inject({}) { |a,e|
|
|
27
|
+
a.merge! e.to_sym => matches[e]
|
|
28
|
+
a
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def controller_for_request request
|
|
33
|
+
controller_class.new params_from(request), controller_method
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def controller_class
|
|
37
|
+
controller_class_name.split(/::/).inject(Object) { |scope, const_name|
|
|
38
|
+
scope.const_get const_name
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def controller_class_name
|
|
43
|
+
controller_action.split(/#/, 2)[0]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def controller_method
|
|
47
|
+
controller_action.split(/#/, 2)[-1]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module AcceptableApi
|
|
2
|
+
class Routes
|
|
3
|
+
attr_accessor :routes
|
|
4
|
+
private :routes=, :routes
|
|
5
|
+
|
|
6
|
+
def initialize routes = []
|
|
7
|
+
self.routes = routes
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def << route
|
|
11
|
+
routes << route
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def for request
|
|
15
|
+
route = routes.detect { |m| m.match? request }
|
|
16
|
+
return MissingRoute.instance unless route
|
|
17
|
+
route
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'acceptance/test_helper'
|
|
2
|
+
|
|
3
|
+
class RequestDifferentMimeTypeVersionsTest < AcceptableApi::AcceptanceTest
|
|
4
|
+
test "the correct representation is returned" do
|
|
5
|
+
header 'Accept', 'application/vnd.acceptable-api.example-v1+txt'
|
|
6
|
+
get '/example/123'
|
|
7
|
+
assert_equal 'Ducks token 123', last_response.body
|
|
8
|
+
assert_equal 'application/vnd.acceptable-api.example-v1+txt', last_response.headers['Content-Type']
|
|
9
|
+
|
|
10
|
+
header 'Accept', 'application/vnd.acceptable-api.example-v2+txt'
|
|
11
|
+
get '/example/123'
|
|
12
|
+
assert_equal 'Chickens token 123', last_response.body
|
|
13
|
+
assert_equal 'application/vnd.acceptable-api.example-v2+txt', last_response.headers['Content-Type']
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# We can't generate an entity body that's acceptable for the client.
|
|
2
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.7
|
|
3
|
+
class RequestUnknowntMimeTypeTest < AcceptableApi::AcceptanceTest
|
|
4
|
+
test "the correct status code is returned" do
|
|
5
|
+
header 'Accept', 'application/pdf'
|
|
6
|
+
get '/example/123'
|
|
7
|
+
assert_equal 406, last_response.status
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
test "a JSON list of alternatives is returned" do
|
|
11
|
+
header 'Accept', 'application/pdf'
|
|
12
|
+
get '/example/123'
|
|
13
|
+
assert_equal 'application/json', last_response.header['Content-Type']
|
|
14
|
+
json = JSON.parse last_response.body
|
|
15
|
+
links = json["links"]
|
|
16
|
+
assert_equal links.size, 2, "I expected only two links"
|
|
17
|
+
v1 = links.detect { |l| l["Content-Type"] == "application/vnd.acceptable-api.example-v1+txt" }
|
|
18
|
+
assert_not_nil v1, "Expected a link with Content-Type 'application/vnd.acceptable-api.example-v1+txt'"
|
|
19
|
+
assert_equal last_request.url, v1['uri']
|
|
20
|
+
assert_equal 'GET', v1['method']
|
|
21
|
+
assert_equal 'alternative', v1['rel']
|
|
22
|
+
|
|
23
|
+
v2 = links.detect { |l| l["Content-Type"] == "application/vnd.acceptable-api.example-v2+txt" }
|
|
24
|
+
assert_not_nil v2, "Expected a link with Content-Type 'application/vnd.acceptable-api.example-v2+txt'"
|
|
25
|
+
assert_equal last_request.url, v2['uri']
|
|
26
|
+
assert_equal 'GET', v2['method']
|
|
27
|
+
assert_equal 'alternative', v2['rel']
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
module AcceptableApi
|
|
4
|
+
class AcceptanceTest < Test::Unit::TestCase
|
|
5
|
+
include Rack::Test::Methods
|
|
6
|
+
|
|
7
|
+
class Example
|
|
8
|
+
attr_accessor :token
|
|
9
|
+
private :token=, :token
|
|
10
|
+
|
|
11
|
+
def initialize token
|
|
12
|
+
self.token = token
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.find token
|
|
16
|
+
# Normally this would load the resource from the data store
|
|
17
|
+
new token
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
"token #{token}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class ExampleResource
|
|
26
|
+
include AcceptableApi::Controller
|
|
27
|
+
|
|
28
|
+
def show
|
|
29
|
+
Example.find params[:token]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def app
|
|
34
|
+
a = AcceptableApi::Builder.new
|
|
35
|
+
a.register Example => 'application/vnd.acceptable-api.example-v1+txt' do |ex|
|
|
36
|
+
'Ducks ' + ex.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
a.register Example => 'application/vnd.acceptable-api.example-v2+txt' do |ex|
|
|
40
|
+
'Chickens ' + ex.to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
a.expose 'AcceptableApi::AcceptanceTest::ExampleResource#show', at: '/example/:token', via: 'get'
|
|
44
|
+
a.to_app
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acceptable_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.4
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,11 +9,27 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2012-
|
|
12
|
+
date: 2012-07-29 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: json
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ! '>='
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
none: false
|
|
26
|
+
requirements:
|
|
27
|
+
- - ! '>='
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '0'
|
|
14
30
|
- !ruby/object:Gem::Dependency
|
|
15
31
|
name: rack
|
|
16
|
-
requirement:
|
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
33
|
none: false
|
|
18
34
|
requirements:
|
|
19
35
|
- - ! '>='
|
|
@@ -21,10 +37,15 @@ dependencies:
|
|
|
21
37
|
version: '0'
|
|
22
38
|
type: :runtime
|
|
23
39
|
prerelease: false
|
|
24
|
-
version_requirements:
|
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ! '>='
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
25
46
|
- !ruby/object:Gem::Dependency
|
|
26
47
|
name: rack-accept
|
|
27
|
-
requirement:
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
28
49
|
none: false
|
|
29
50
|
requirements:
|
|
30
51
|
- - ! '>='
|
|
@@ -32,10 +53,15 @@ dependencies:
|
|
|
32
53
|
version: '0'
|
|
33
54
|
type: :runtime
|
|
34
55
|
prerelease: false
|
|
35
|
-
version_requirements:
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
none: false
|
|
58
|
+
requirements:
|
|
59
|
+
- - ! '>='
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
36
62
|
- !ruby/object:Gem::Dependency
|
|
37
63
|
name: rack-accept-header-updater
|
|
38
|
-
requirement:
|
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
|
39
65
|
none: false
|
|
40
66
|
requirements:
|
|
41
67
|
- - ! '>='
|
|
@@ -43,20 +69,62 @@ dependencies:
|
|
|
43
69
|
version: '0'
|
|
44
70
|
type: :runtime
|
|
45
71
|
prerelease: false
|
|
46
|
-
version_requirements:
|
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
73
|
+
none: false
|
|
74
|
+
requirements:
|
|
75
|
+
- - ! '>='
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
47
78
|
- !ruby/object:Gem::Dependency
|
|
48
|
-
name:
|
|
49
|
-
requirement:
|
|
79
|
+
name: rack-test
|
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
81
|
none: false
|
|
51
82
|
requirements:
|
|
52
83
|
- - ! '>='
|
|
53
84
|
- !ruby/object:Gem::Version
|
|
54
85
|
version: '0'
|
|
55
|
-
type: :
|
|
86
|
+
type: :development
|
|
56
87
|
prerelease: false
|
|
57
|
-
version_requirements:
|
|
58
|
-
|
|
59
|
-
|
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
89
|
+
none: false
|
|
90
|
+
requirements:
|
|
91
|
+
- - ! '>='
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
94
|
+
- !ruby/object:Gem::Dependency
|
|
95
|
+
name: test-unit
|
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
|
97
|
+
none: false
|
|
98
|
+
requirements:
|
|
99
|
+
- - ! '>='
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0'
|
|
102
|
+
type: :development
|
|
103
|
+
prerelease: false
|
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
105
|
+
none: false
|
|
106
|
+
requirements:
|
|
107
|
+
- - ! '>='
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rake
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
none: false
|
|
114
|
+
requirements:
|
|
115
|
+
- - ! '>='
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
none: false
|
|
122
|
+
requirements:
|
|
123
|
+
- - ! '>='
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0'
|
|
126
|
+
description: HTTP lets clients send Accept headers. We should probably use that to
|
|
127
|
+
work out what they'll accept as a response, yea?
|
|
60
128
|
email:
|
|
61
129
|
- craig@barkingiguana.com
|
|
62
130
|
executables: []
|
|
@@ -64,6 +132,7 @@ extensions: []
|
|
|
64
132
|
extra_rdoc_files: []
|
|
65
133
|
files:
|
|
66
134
|
- .gitignore
|
|
135
|
+
- .rvmrc
|
|
67
136
|
- Gemfile
|
|
68
137
|
- LICENSE
|
|
69
138
|
- README.md
|
|
@@ -75,7 +144,23 @@ files:
|
|
|
75
144
|
- example/config.ru
|
|
76
145
|
- example/example.rb
|
|
77
146
|
- lib/acceptable_api.rb
|
|
147
|
+
- lib/acceptable_api/accepts.rb
|
|
148
|
+
- lib/acceptable_api/action.rb
|
|
149
|
+
- lib/acceptable_api/application.rb
|
|
150
|
+
- lib/acceptable_api/builder.rb
|
|
151
|
+
- lib/acceptable_api/controller.rb
|
|
152
|
+
- lib/acceptable_api/mapper.rb
|
|
153
|
+
- lib/acceptable_api/mappers.rb
|
|
154
|
+
- lib/acceptable_api/missing_controller.rb
|
|
155
|
+
- lib/acceptable_api/missing_mapper.rb
|
|
156
|
+
- lib/acceptable_api/missing_route.rb
|
|
157
|
+
- lib/acceptable_api/route.rb
|
|
158
|
+
- lib/acceptable_api/routes.rb
|
|
78
159
|
- lib/acceptable_api/version.rb
|
|
160
|
+
- test/acceptance/request_different_mime_type_versions_test.rb
|
|
161
|
+
- test/acceptance/request_unknown_mime_type_test.rb
|
|
162
|
+
- test/acceptance/test_helper.rb
|
|
163
|
+
- test/test_helper.rb
|
|
79
164
|
homepage: http://barkingiguana.com/2011/12/05/principles-of-service-design-program-to-an-interface/
|
|
80
165
|
licenses: []
|
|
81
166
|
post_install_message:
|
|
@@ -88,16 +173,26 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
88
173
|
- - ! '>='
|
|
89
174
|
- !ruby/object:Gem::Version
|
|
90
175
|
version: '0'
|
|
176
|
+
segments:
|
|
177
|
+
- 0
|
|
178
|
+
hash: 4567978281283614839
|
|
91
179
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
180
|
none: false
|
|
93
181
|
requirements:
|
|
94
182
|
- - ! '>='
|
|
95
183
|
- !ruby/object:Gem::Version
|
|
96
184
|
version: '0'
|
|
185
|
+
segments:
|
|
186
|
+
- 0
|
|
187
|
+
hash: 4567978281283614839
|
|
97
188
|
requirements: []
|
|
98
189
|
rubyforge_project:
|
|
99
|
-
rubygems_version: 1.8.
|
|
190
|
+
rubygems_version: 1.8.24
|
|
100
191
|
signing_key:
|
|
101
192
|
specification_version: 3
|
|
102
193
|
summary: Build an Acceptable API
|
|
103
|
-
test_files:
|
|
194
|
+
test_files:
|
|
195
|
+
- test/acceptance/request_different_mime_type_versions_test.rb
|
|
196
|
+
- test/acceptance/request_unknown_mime_type_test.rb
|
|
197
|
+
- test/acceptance/test_helper.rb
|
|
198
|
+
- test/test_helper.rb
|