versioncake 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/README.md +162 -7
- data/images/versioncake-logo450x100.png +0 -0
- data/lib/versioncake/action_controller/versioning.rb +12 -7
- data/lib/versioncake/version.rb +1 -1
- data/test/functional/renders_controller_test.rb +32 -1
- metadata +3 -2
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,10 +1,23 @@
|
|
1
|
-
|
1
|
+
![Version Cake](https://raw.github.com/alicial/versioncake/master/images/versioncake-logo450x100.png)
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/bwillis/versioncake.png?branch=master)](http://travis-ci.org/bwillis/versioncake)
|
2
4
|
|
3
5
|
Co-authored by Ben Willis ([@bwillis](https://github.com/bwillis/)) and Jim Jones ([@aantix](https://github.com/aantix)).
|
4
6
|
|
5
|
-
Version Cake is an unobtrusive way to version
|
7
|
+
Version Cake is an unobtrusive way to version APIs in your Rails app.
|
6
8
|
|
7
|
-
|
9
|
+
- Easily version any view with their API version:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
app/views/posts/
|
13
|
+
- index.v1.xml.builder
|
14
|
+
- index.v3.xml.builder
|
15
|
+
- index.v1.json.jbuilder
|
16
|
+
- index.v4.json.jbuilder
|
17
|
+
```
|
18
|
+
- Gracefully degrade requests to the latest supported version
|
19
|
+
- Clients can request API versions through different strategies
|
20
|
+
- Dry your controller logic with exposed helpers
|
8
21
|
|
9
22
|
## Install
|
10
23
|
|
@@ -12,6 +25,134 @@ We were tired of urls with version numbers, namespacing controllers with version
|
|
12
25
|
gem install versioncake
|
13
26
|
```
|
14
27
|
|
28
|
+
## Example
|
29
|
+
|
30
|
+
In this simple example we will outline the code that is introduced to support a change in a version.
|
31
|
+
|
32
|
+
### config/application.rb
|
33
|
+
```ruby
|
34
|
+
config.view_versions = (1...4)
|
35
|
+
config.view_version_extraction_strategy = :http_parameter # for simplicity
|
36
|
+
```
|
37
|
+
|
38
|
+
Often times with APIs, depending upon the version, different logic needs to be applied. With the following controller code, the initial value of @posts includes all Post entries.
|
39
|
+
But if the requested API version is three or greater, we're going to eagerly load the associated comments as well.
|
40
|
+
|
41
|
+
Being able to control the logic based on the api version allow you to ensure forwards and backwards compatibility for future changes.
|
42
|
+
|
43
|
+
### PostsController
|
44
|
+
```ruby
|
45
|
+
class PostsController < ApplicationController
|
46
|
+
def index
|
47
|
+
# shared code for all versions
|
48
|
+
@posts = Post.scoped
|
49
|
+
|
50
|
+
# version 3 or greated supports embedding post comments
|
51
|
+
if requested_version >= 3
|
52
|
+
@posts = @posts.includes(:comments)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
See the view samples below. The basic top level posts are referenced in views/posts/index.v1.json.jbuilder.
|
59
|
+
But for views/posts/index.v4.json.jbuilder, we utilize the additional related comments.
|
60
|
+
|
61
|
+
### Views
|
62
|
+
|
63
|
+
Notice the version numbers are denoted by the "v{version number}" extension within the file name.
|
64
|
+
|
65
|
+
#### views/posts/index.v1.json.jbuilder
|
66
|
+
```ruby
|
67
|
+
json.array!(@posts) do |json, post|
|
68
|
+
json.(post, :id, :title)
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
#### views/posts/index.v4.json.jbuilder
|
73
|
+
```ruby
|
74
|
+
json.array!(@posts) do |json, post|
|
75
|
+
json.(post, :id, :title)
|
76
|
+
json.comments post.comments, :id, :text
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
### Sample Output
|
81
|
+
|
82
|
+
When a version is specified for which a view doesn't exist, the request degrades and renders the next lowest version number to ensure the API's backwards compatibility. In the following case, since views/posts/index.v3.json.jbuilder doesn't exist, views/posts/index.v1.json.jbuilder is rendered instead.
|
83
|
+
|
84
|
+
#### http://localhost:3000/posts.json?api_version=3
|
85
|
+
```javascript
|
86
|
+
[
|
87
|
+
{
|
88
|
+
id: 1
|
89
|
+
title: "Version Cake v0.1.0 Released!"
|
90
|
+
name: "Ben"
|
91
|
+
updated_at: "2012-09-17T16:23:45Z"
|
92
|
+
},
|
93
|
+
{
|
94
|
+
id: 2
|
95
|
+
title: "Version Cake v0.2.0 Released!"
|
96
|
+
name: "Jim"
|
97
|
+
updated_at: "2012-09-17T16:23:32Z"
|
98
|
+
}
|
99
|
+
]
|
100
|
+
```
|
101
|
+
|
102
|
+
|
103
|
+
For a given request, if we specify the version number, and that version of the view exists, that version specific view version will be rendered. In the below case, views/posts/index.v1.json.jbuilder is rendered.
|
104
|
+
|
105
|
+
#### http://localhost:3000/posts.json?api_version=2 or http://localhost:3000/posts.json?api_version=1
|
106
|
+
```javascript
|
107
|
+
[
|
108
|
+
{
|
109
|
+
id: 1
|
110
|
+
title: "Version Cake v0.1.0 Released!"
|
111
|
+
name: "Ben"
|
112
|
+
updated_at: "2012-09-17T16:23:45Z"
|
113
|
+
},
|
114
|
+
{
|
115
|
+
id: 2
|
116
|
+
title: "Version Cake v0.2.0 Released!"
|
117
|
+
name: "Jim"
|
118
|
+
updated_at: "2012-09-17T16:23:32Z"
|
119
|
+
}
|
120
|
+
]
|
121
|
+
```
|
122
|
+
|
123
|
+
|
124
|
+
When no version is specified, the latest version of the view is rendered. In this case, views/posts/index.v4.json.jbuilder.
|
125
|
+
|
126
|
+
#### http://localhost:3000/posts.json
|
127
|
+
```javascript
|
128
|
+
[
|
129
|
+
{
|
130
|
+
id: 1
|
131
|
+
title: "Version Cake v0.1.0 Released!"
|
132
|
+
name: "Ben"
|
133
|
+
updated_at: "2012-09-17T16:23:45Z"
|
134
|
+
comments: [
|
135
|
+
{
|
136
|
+
id: 1
|
137
|
+
text: "Woah interesting approach on versioning"
|
138
|
+
}
|
139
|
+
]
|
140
|
+
},
|
141
|
+
{
|
142
|
+
id: 2
|
143
|
+
title: "Version Cake v0.2.0 Released!"
|
144
|
+
name: "Jim"
|
145
|
+
updated_at: "2012-09-17T16:23:32Z"
|
146
|
+
comments: [
|
147
|
+
{
|
148
|
+
id: 4
|
149
|
+
text: "These new features are greeeeat!"
|
150
|
+
}
|
151
|
+
]
|
152
|
+
}
|
153
|
+
]
|
154
|
+
```
|
155
|
+
|
15
156
|
## How to use
|
16
157
|
|
17
158
|
### Configuration
|
@@ -23,12 +164,12 @@ config.view_versions = [1,3,4,5] # or (1...5)
|
|
23
164
|
```
|
24
165
|
You can also define the way to extract the version. The `view_version_extraction_strategy` allows you to set one of the default strategies or provide a proc to set your own. You can also pass it a prioritized array of the strategies.
|
25
166
|
```ruby
|
26
|
-
config.view_version_extraction_strategy = :
|
167
|
+
config.view_version_extraction_strategy = :query_parameter # [:http_header, :http_accept_parameter]
|
27
168
|
```
|
28
|
-
These are the
|
169
|
+
These are the available strategies:
|
170
|
+
- **query_parameter**: version in the url query parameter, for testing or to override for special case i.e. ```http://localhost:3000/posts.json?api_version=1``` (This is the default.)
|
29
171
|
- **http_header**: Api version HTTP header ie. ```API-Version: 1```
|
30
172
|
- **http_accept_parameter**: HTTP Accept header ie. ```Accept: application/xml; version=1``` [why do this?](http://blog.steveklabnik.com/posts/2011-07-03-nobody-understands-rest-or-http#i_want_my_api_to_be_versioned)
|
31
|
-
- **query_parameter**: version in the url query parameter, for testing or to override for special case ie. ```http://localhost:3000/posts.json?api_version=1```
|
32
173
|
- **custom**: `lambda {|request| request.headers["HTTP_X_API_VERSION"].to_i }` takes the request object and must return an integer
|
33
174
|
|
34
175
|
### Version your views
|
@@ -52,7 +193,17 @@ If you start supporting a newer version, v3 for instance, you do not have to cop
|
|
52
193
|
### Controller
|
53
194
|
|
54
195
|
You don't need to do anything special in your controller, but if you find that you want to perform some tasks for a specific version you can use `requested_version` and `latest_version`. This may be updated in the [near future](https://github.com/bwillis/versioncake/issues/1).
|
55
|
-
|
196
|
+
```ruby
|
197
|
+
def index
|
198
|
+
# shared code for all versions
|
199
|
+
@posts = Post.scoped
|
200
|
+
|
201
|
+
# version 3 or greated supports embedding post comments
|
202
|
+
if requested_version >= 3
|
203
|
+
@posts = @posts.includes(:comments)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
56
207
|
### Client requests
|
57
208
|
|
58
209
|
When a client makes a request it will automatically receive the latest supported version of the view. The client can also request for a specific version by one of the strategies configured by ``view_version_extraction_strategy``.
|
@@ -63,11 +214,15 @@ When a client makes a request it will automatically receive the latest supported
|
|
63
214
|
|
64
215
|
- https://github.com/bploetz/versionist
|
65
216
|
- https://github.com/filtersquad/rocket_pants
|
217
|
+
- https://github.com/lyonrb/biceps
|
66
218
|
|
67
219
|
## Discussions
|
68
220
|
|
69
221
|
- [Steve Klabnik on how to version in a resful way](http://blog.steveklabnik.com/posts/2011-07-03-nobody-understands-rest-or-http#i_want_my_api_to_be_versioned)
|
70
222
|
- [Rails API project disucssion on versioning](https://github.com/spastorino/rails-api/issues/8)
|
223
|
+
- [Railscast on versioning](http://railscasts.com/episodes/350-rest-api-versioning)
|
224
|
+
- [Ruby5 on versioncake](http://ruby5.envylabs.com/episodes/310-episode-306-september-18th-2012/stories/2704-version-cake)
|
225
|
+
- [Rails core discussion](https://groups.google.com/forum/#!msg/rubyonrails-core/odwmEYYIum0/9POep66BvoMJ)
|
71
226
|
|
72
227
|
# Questions?
|
73
228
|
|
Binary file
|
@@ -4,7 +4,7 @@ module ActionController #:nodoc:
|
|
4
4
|
module Versioning
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
|
-
attr_accessor :requested_version, :is_latest_version
|
7
|
+
attr_accessor :requested_version, :is_latest_version, :derived_version
|
8
8
|
|
9
9
|
included do
|
10
10
|
prepend_before_filter :set_version
|
@@ -12,14 +12,19 @@ module ActionController #:nodoc:
|
|
12
12
|
|
13
13
|
protected
|
14
14
|
def set_version
|
15
|
-
requested_version = ActionView::Template::Versions.extract_version request
|
16
|
-
|
17
|
-
|
18
|
-
@
|
15
|
+
@requested_version = ActionView::Template::Versions.extract_version request
|
16
|
+
|
17
|
+
if @requested_version.nil?
|
18
|
+
@derived_version = ActionView::Template::Versions.latest_version
|
19
|
+
elsif ActionView::Template::Versions.supports_version? requested_version
|
20
|
+
@derived_version = @requested_version
|
21
|
+
elsif requested_version > ActionView::Template::Versions.latest_version
|
22
|
+
raise ActionController::RoutingError.new("No route match for version")
|
19
23
|
else
|
20
|
-
|
24
|
+
raise ActionController::RoutingError.new("Version is deprecated")
|
21
25
|
end
|
22
|
-
@
|
26
|
+
@_lookup_context.versions = ActionView::Template::Versions.supported_versions(@derived_version)
|
27
|
+
@is_latest_version = @derived_version == ActionView::Template::Versions.latest_version
|
23
28
|
end
|
24
29
|
end
|
25
30
|
end
|
data/lib/versioncake/version.rb
CHANGED
@@ -15,7 +15,7 @@ class RendersControllerTest < ActionController::TestCase
|
|
15
15
|
end
|
16
16
|
|
17
17
|
test "exposes latest version when requesting the latest" do
|
18
|
-
get :index, "api_version" => "
|
18
|
+
get :index, "api_version" => "3"
|
19
19
|
assert @controller.is_latest_version
|
20
20
|
end
|
21
21
|
|
@@ -23,6 +23,16 @@ class RendersControllerTest < ActionController::TestCase
|
|
23
23
|
get :index, "api_version" => "1"
|
24
24
|
assert !@controller.is_latest_version
|
25
25
|
end
|
26
|
+
|
27
|
+
test "exposes the derived version when the version is not set" do
|
28
|
+
get :index
|
29
|
+
assert_equal 3, @controller.derived_version
|
30
|
+
end
|
31
|
+
|
32
|
+
test "requested version is blank when the version is not set" do
|
33
|
+
get :index
|
34
|
+
assert_blank @controller.requested_version
|
35
|
+
end
|
26
36
|
end
|
27
37
|
|
28
38
|
class ParameterStragegyTest < ActionController::TestCase
|
@@ -142,4 +152,25 @@ class MultipleStrategyTest < ActionController::TestCase
|
|
142
152
|
get :index, "api_version" => "1"
|
143
153
|
assert_equal @response.body, "index.v2.html.erb"
|
144
154
|
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class UnsupportedVersionTest < ActionController::TestCase
|
158
|
+
tests RendersController
|
159
|
+
|
160
|
+
setup do
|
161
|
+
ActionView::Template::Versions.extraction_strategy = :query_parameter
|
162
|
+
end
|
163
|
+
|
164
|
+
test "responds with 404 when the version is larger than the supported version" do
|
165
|
+
assert_raise ActionController::RoutingError do
|
166
|
+
get :index, "api_version" => "4"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
test "responds with 404 when the version is lower than the latest version, but not an available version" do
|
171
|
+
ActionView::Template::Versions.supported_version_numbers = [2,3]
|
172
|
+
assert_raise ActionController::RoutingError do
|
173
|
+
get :index, "api_version" => "1"
|
174
|
+
end
|
175
|
+
end
|
145
176
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: versioncake
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2012-
|
13
|
+
date: 2012-10-14 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: actionpack
|
@@ -106,6 +106,7 @@ files:
|
|
106
106
|
- Gemfile.lock
|
107
107
|
- README.md
|
108
108
|
- Rakefile
|
109
|
+
- images/versioncake-logo450x100.png
|
109
110
|
- lib/versioncake.rb
|
110
111
|
- lib/versioncake/action_controller/versioning.rb
|
111
112
|
- lib/versioncake/action_view/lookup_context.rb
|