gates 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +94 -0
- data/Rakefile +2 -0
- data/gates.gemspec +24 -0
- data/lib/gates.rb +26 -0
- data/lib/gates/api_version.rb +17 -0
- data/lib/gates/gate.rb +9 -0
- data/lib/gates/manifest.rb +32 -0
- data/lib/gates/version.rb +3 -0
- data/spec/gates/api_version_spec.rb +49 -0
- data/spec/gates/manifest_spec.rb +41 -0
- data/spec/gates_spec.rb +31 -0
- data/spec/spec_helper.rb +2 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4d315c3d1a09f58d881ff8c9a8c9653bcd76bfa2
|
4
|
+
data.tar.gz: 8e530fdd6543793fcaa443dbb2a08305d2942ebe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9810eed9e438a0b213f54eefa9c5e7435aec3541240caa52a7ccffc80eaa576bdd9d5c7785e1ae719a86dd35786bbace8caaf84f7d62f0946b5e728b351e7aeb
|
7
|
+
data.tar.gz: 17bf56e8bd2114185b8ff1817f60c3ccc8be8d9a60440fc9ef0f414d843145b559e9120aba616bd38924247480439c59dfb3935efd65a2727955b6cc180cf3b4
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
sudo: false
|
2
|
+
language: ruby
|
3
|
+
cache: bundler
|
4
|
+
before_script:
|
5
|
+
- 'echo ''gem: --no-ri --no-rdoc'' > ~/.gemrc' # skip installing docs for gems
|
6
|
+
before_install:
|
7
|
+
- gem install bundler -v ">= 1.7.0" # Minimum version of bundler required
|
8
|
+
script: 'bundle exec rspec'
|
9
|
+
rvm:
|
10
|
+
- 2.1
|
11
|
+
- 2.2.2
|
12
|
+
- 2.3.0
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2016 Phill Baker
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Gates
|
2
|
+
|
3
|
+
[](https://travis-ci.org/phillbaker/gates)
|
5
|
+
|
6
|
+
This is an implementation of the ideas expressed in this post about Stripe's public API versioning, [Move fast, don't break your API](http://amberonrails.com/move-fast-dont-break-your-api/). The goal is to separate layers of request and response compatibility from the API logic, with two important drivers:
|
7
|
+
* let your customers migrate API versions at their convenience, minimize the pain of upgrades when they do upgrade.
|
8
|
+
* make it feasible to fix backward incompatible API schema mistakes (they will happen).
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'gates'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install gates
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
Check for whether a gate is enabled:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
version = request.api_version || user.api_version
|
32
|
+
version = Gates.for(version)
|
33
|
+
version.enabled?(:foo) # => true / false
|
34
|
+
|
35
|
+
Gates.available_versions # => [<Gates::ApiVersion ...>]
|
36
|
+
```
|
37
|
+
|
38
|
+
In CI make sure to lint the version manifest:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
rake gates:lint
|
42
|
+
```
|
43
|
+
|
44
|
+
This can be combined with A/B testing or feature flipping like:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
def foo
|
48
|
+
return unless feature_enabled?(user, :foo) || ab_test?(:foo) || version.enabled?(:foo)
|
49
|
+
|
50
|
+
"foo"
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
```yaml
|
55
|
+
----
|
56
|
+
versions:
|
57
|
+
-
|
58
|
+
id: 2016-01-30-1
|
59
|
+
gates:
|
60
|
+
-
|
61
|
+
name: reallows_amount
|
62
|
+
description: |
|
63
|
+
Just kidding. Sending amount is supported for some folks.
|
64
|
+
-
|
65
|
+
id: 2016-01-30
|
66
|
+
gates:
|
67
|
+
-
|
68
|
+
name: disallows_amount
|
69
|
+
description: |
|
70
|
+
Sending amount is now deprecated.
|
71
|
+
-
|
72
|
+
id: 2016-01-20
|
73
|
+
gates:
|
74
|
+
-
|
75
|
+
name: allows_amount
|
76
|
+
description: |
|
77
|
+
Sending amount is supported.
|
78
|
+
```
|
79
|
+
|
80
|
+
### Testing
|
81
|
+
|
82
|
+
Make sure to test both paths in order to not break compatibility!
|
83
|
+
|
84
|
+
## Similar projects
|
85
|
+
|
86
|
+
* [multiverse in Elixir](https://github.com/Nebo15/multiverse)
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
1. Fork it ( https://github.com/phillbaker/gates/fork )
|
91
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
92
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
93
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
94
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/gates.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'gates/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "gates"
|
8
|
+
spec.version = Gates::VERSION
|
9
|
+
spec.authors = ["Phillip Baker"]
|
10
|
+
spec.email = ["phillbaker@retrodict.com"]
|
11
|
+
spec.summary = %q{Permanent backwards compatibility for changing APIs.}
|
12
|
+
spec.homepage = "https://github.com/phillbaker/gates"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
spec.required_ruby_version = ">= 2.0.0"
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
end
|
data/lib/gates.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
require "gates/api_version"
|
4
|
+
require "gates/gate"
|
5
|
+
require "gates/manifest"
|
6
|
+
require "gates/version"
|
7
|
+
|
8
|
+
module Gates
|
9
|
+
Error = Class.new(StandardError)
|
10
|
+
UninitializedError = Class.new(Gates::Error)
|
11
|
+
|
12
|
+
class<<self
|
13
|
+
attr_accessor :manifest
|
14
|
+
|
15
|
+
def load(file_path)
|
16
|
+
hash = Psych.parse_file(file_path)
|
17
|
+
@manifest = Manifest.new(hash)
|
18
|
+
end
|
19
|
+
|
20
|
+
def for(version_id)
|
21
|
+
raise UninitializedError unless @manifest
|
22
|
+
|
23
|
+
@manifest[version_id]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Gates
|
2
|
+
class ApiVersion
|
3
|
+
attr_accessor :id, :gates, :predecessor
|
4
|
+
|
5
|
+
def enabled?(gate_name)
|
6
|
+
if gates.include?(gate_name)
|
7
|
+
true
|
8
|
+
elsif !predecessor.nil?
|
9
|
+
# recurse to check list of all gates less than or equal to this api
|
10
|
+
# version
|
11
|
+
!!predecessor.enabled?(gate_name)
|
12
|
+
else
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/gates/gate.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Gates
|
2
|
+
class Manifest
|
3
|
+
attr_accessor :version_map
|
4
|
+
|
5
|
+
def initialize(manifest_hash)
|
6
|
+
versions = []
|
7
|
+
# iterate backward through the versions to be able to associate the
|
8
|
+
# predecessor with each
|
9
|
+
manifest_hash['versions'].reverse.each do |version_info|
|
10
|
+
version = ApiVersion.new
|
11
|
+
version.id = version_info['id']
|
12
|
+
version.gates = version_info['gates'].map do |gate_info|
|
13
|
+
gate = Gate.new
|
14
|
+
gate.name = gate_info['name']
|
15
|
+
gate
|
16
|
+
end
|
17
|
+
version.predecessor = versions.first
|
18
|
+
|
19
|
+
versions << version
|
20
|
+
end
|
21
|
+
|
22
|
+
self.version_map = {}
|
23
|
+
versions.each do |version|
|
24
|
+
version_map[version.id] = version
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](version_id)
|
29
|
+
version_map[version_id]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Gates::ApiVersion do
|
6
|
+
describe "#enabled?" do
|
7
|
+
let(:initial_version) do
|
8
|
+
version = Gates::ApiVersion.new
|
9
|
+
version.id = "2016-01-29"
|
10
|
+
gate = Gates::Gate.new
|
11
|
+
gate.name = "allows_foo"
|
12
|
+
version.gates = [gate]
|
13
|
+
version
|
14
|
+
end
|
15
|
+
let(:later_version) do
|
16
|
+
version = Gates::ApiVersion.new
|
17
|
+
version.id = "2016-01-30"
|
18
|
+
gate = Gates::Gate.new
|
19
|
+
gate.name = "allows_bar"
|
20
|
+
version.gates = [gate]
|
21
|
+
version.predecessor = initial_version
|
22
|
+
version
|
23
|
+
end
|
24
|
+
|
25
|
+
context "without predecessor" do
|
26
|
+
it "is true for existing gates" do
|
27
|
+
expect(initial_version.enabled?("allows_foo")).to be_truthy
|
28
|
+
end
|
29
|
+
|
30
|
+
it "is false for non-existing gates" do
|
31
|
+
expect(initial_version.enabled?("allows_cat")).to be_falsey
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "with predecessor" do
|
36
|
+
it "is true for existing gates" do
|
37
|
+
expect(later_version.enabled?("allows_bar")).to be_truthy
|
38
|
+
end
|
39
|
+
|
40
|
+
it "is true for predecessor existing gates" do
|
41
|
+
expect(later_version.enabled?("allows_foo")).to be_truthy
|
42
|
+
end
|
43
|
+
|
44
|
+
it "is false for non-existing gates" do
|
45
|
+
expect(later_version.enabled?("allows_cat")).to be_falsey
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Gates::Manifest do
|
4
|
+
describe "#initialize" do
|
5
|
+
let(:manifest_data) {
|
6
|
+
{
|
7
|
+
"versions" => [
|
8
|
+
{
|
9
|
+
"id" => "2016-01-30",
|
10
|
+
"gates" => [
|
11
|
+
"name" => "allows_bar"
|
12
|
+
]
|
13
|
+
},
|
14
|
+
{
|
15
|
+
"id" => "2016-01-29",
|
16
|
+
"gates" => [
|
17
|
+
"name" => "allows_foo"
|
18
|
+
]
|
19
|
+
},
|
20
|
+
]
|
21
|
+
}
|
22
|
+
}
|
23
|
+
let(:manifest) { Gates::Manifest.new(manifest_data) }
|
24
|
+
|
25
|
+
it "fills the version_map" do
|
26
|
+
expect(manifest.version_map.size).to eq 2
|
27
|
+
end
|
28
|
+
|
29
|
+
it "sets the api version gates" do
|
30
|
+
verison = manifest.version_map["2016-01-30"]
|
31
|
+
expect(verison.gates.size).to eq 1
|
32
|
+
end
|
33
|
+
|
34
|
+
it "calculates the version predecessor" do
|
35
|
+
earlier_version = manifest.version_map["2016-01-30"].predecessor
|
36
|
+
expect(earlier_version.id).to eq "2016-01-29"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#[]"
|
41
|
+
end
|
data/spec/gates_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Gates do
|
4
|
+
it "has a version number" do
|
5
|
+
expect(Gates::VERSION).not_to be nil
|
6
|
+
end
|
7
|
+
|
8
|
+
describe ".for" do
|
9
|
+
let(:manifest_data) {
|
10
|
+
{
|
11
|
+
"versions" => [
|
12
|
+
{
|
13
|
+
"id" => "2016-01-30",
|
14
|
+
"gates" => [
|
15
|
+
"name" => "allows_special"
|
16
|
+
]
|
17
|
+
},
|
18
|
+
]
|
19
|
+
}
|
20
|
+
}
|
21
|
+
let(:manifest) { Gates::Manifest.new(manifest_data) }
|
22
|
+
|
23
|
+
it "returns an api version" do
|
24
|
+
Gates.manifest = manifest
|
25
|
+
expect(Gates.for("2016-01-30")).to be_a(Gates::ApiVersion)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "returns nil if no api version exists"
|
29
|
+
it "raises on uninitialized"
|
30
|
+
end
|
31
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gates
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Phillip Baker
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- phillbaker@retrodict.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- ".travis.yml"
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE.txt
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- gates.gemspec
|
70
|
+
- lib/gates.rb
|
71
|
+
- lib/gates/api_version.rb
|
72
|
+
- lib/gates/gate.rb
|
73
|
+
- lib/gates/manifest.rb
|
74
|
+
- lib/gates/version.rb
|
75
|
+
- spec/gates/api_version_spec.rb
|
76
|
+
- spec/gates/manifest_spec.rb
|
77
|
+
- spec/gates_spec.rb
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
homepage: https://github.com/phillbaker/gates
|
80
|
+
licenses:
|
81
|
+
- MIT
|
82
|
+
metadata: {}
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: 2.0.0
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 2.4.8
|
100
|
+
signing_key:
|
101
|
+
specification_version: 4
|
102
|
+
summary: Permanent backwards compatibility for changing APIs.
|
103
|
+
test_files:
|
104
|
+
- spec/gates/api_version_spec.rb
|
105
|
+
- spec/gates/manifest_spec.rb
|
106
|
+
- spec/gates_spec.rb
|
107
|
+
- spec/spec_helper.rb
|