flipper-api 0.15.0 → 0.16.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae54a392fa555bc9ee34b7783aff686a17bc9543
4
- data.tar.gz: 21c6d2a34ed3789aaea62c84bb86a3558459716c
3
+ metadata.gz: 1fe848186acddf2d53ab3068d5f2ec2246ab688c
4
+ data.tar.gz: 20c08ae092d4b61c9a1412778a55f84ca9602c1b
5
5
  SHA512:
6
- metadata.gz: 09ce98c6e834ac45e3b46c4d47936f1cefe5e45facb26ccc1e2e183f45748d32190c7ee75f9cccb37ca3a11b0872af36af9b7f8360c7fa5463f248883d97f6f3
7
- data.tar.gz: 1c553a7e7f0acfd12e158f8211ae79b72941317093201e2d73dce235eaa4660e5cba4c108f661bb48c93d834880cdf6f15af4776de8ad1447d31486332296d85
6
+ metadata.gz: 8e7e1c899f4a5cd09ac3e129e8e83d0085a46b1ccb4bd05a6d849ad4ccc7a4a158313a08d22201d644f4d35820d70482b248e1a81283f5434ccfc93e70b88863
7
+ data.tar.gz: 4d367bab12c168329609aa4c9cac1b7098bd68d428d49d927c4c5ee22584864482877866a49d733c9a36fa4874d698b632277cff90ee027f800a0196343febbd
@@ -1,5 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  require File.expand_path('../lib/flipper/version', __FILE__)
3
+ require File.expand_path('../lib/flipper/metadata', __FILE__)
3
4
 
4
5
  flipper_api_files = lambda do |file|
5
6
  file =~ %r{(flipper)[\/-]api}
@@ -17,6 +18,7 @@ Gem::Specification.new do |gem|
17
18
  gem.name = 'flipper-api'
18
19
  gem.require_paths = ['lib']
19
20
  gem.version = Flipper::VERSION
21
+ gem.metadata = Flipper::METADATA
20
22
 
21
23
  gem.add_dependency 'rack', '>= 1.4', '< 3'
22
24
  gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
@@ -1,7 +1,5 @@
1
1
  require 'rack'
2
2
  require 'flipper'
3
- require 'flipper/middleware/setup_env'
4
- require 'flipper/middleware/memoizer'
5
3
  require 'flipper/api/middleware'
6
4
  require 'flipper/api/json_params'
7
5
 
@@ -6,6 +6,16 @@ require 'json'
6
6
  module Flipper
7
7
  module Api
8
8
  class Action
9
+ module FeatureNameFromRoute
10
+ def feature_name
11
+ @feature_name ||= begin
12
+ match = request.path_info.match(self.class.route_regex)
13
+ match ? match[:feature_name] : nil
14
+ end
15
+ end
16
+ private :feature_name
17
+ end
18
+
9
19
  extend Forwardable
10
20
 
11
21
  VALID_REQUEST_METHOD_NAMES = Set.new([
@@ -21,7 +31,17 @@ module Flipper
21
31
  #
22
32
  # Returns nothing.
23
33
  def self.route(regex)
24
- @regex = regex
34
+ @route_regex = regex
35
+ end
36
+
37
+ # Internal: Does this action's route match the path.
38
+ def self.route_match?(path)
39
+ path.match(route_regex)
40
+ end
41
+
42
+ # Internal: The regex that matches which routes this action will work for.
43
+ def self.route_regex
44
+ @route_regex || raise("#{name}.route is not set")
25
45
  end
26
46
 
27
47
  # Internal: Initializes and runs an action for a given request.
@@ -34,11 +54,6 @@ module Flipper
34
54
  new(flipper, request).run
35
55
  end
36
56
 
37
- # Internal: The regex that matches which routes this action will work for.
38
- def self.regex
39
- @regex || raise("#{name}.route is not set")
40
- end
41
-
42
57
  # Public: The instance of the Flipper::DSL the middleware was
43
58
  # initialized with.
44
59
  attr_reader :flipper
@@ -1,5 +1,6 @@
1
1
  module Flipper
2
2
  module Api
3
+ # Internal: Used to detect the action that should be used in the middleware.
3
4
  class ActionCollection
4
5
  def initialize
5
6
  @action_classes = []
@@ -11,7 +12,7 @@ module Flipper
11
12
 
12
13
  def action_for_request(request)
13
14
  @action_classes.detect do |action_class|
14
- request.path_info =~ action_class.regex
15
+ action_class.route_match?(request.path_info)
15
16
  end
16
17
  end
17
18
  end
@@ -6,7 +6,9 @@ module Flipper
6
6
  module V1
7
7
  module Actions
8
8
  class ActorsGate < Api::Action
9
- route %r{features/[^/]*/actors/?\Z}
9
+ include FeatureNameFromRoute
10
+
11
+ route %r{\A/features/(?<feature_name>.*)/actors/?\Z}
10
12
 
11
13
  def post
12
14
  ensure_valid_params
@@ -32,10 +34,6 @@ module Flipper
32
34
  json_error_response(:flipper_id_invalid) if flipper_id.nil?
33
35
  end
34
36
 
35
- def feature_name
36
- @feature_name ||= Rack::Utils.unescape(path_parts[-2])
37
- end
38
-
39
37
  def flipper_id
40
38
  @flipper_id ||= params['flipper_id']
41
39
  end
@@ -6,10 +6,11 @@ module Flipper
6
6
  module V1
7
7
  module Actions
8
8
  class BooleanGate < Api::Action
9
- route %r{features/[^/]*/boolean/?\Z}
9
+ include FeatureNameFromRoute
10
+
11
+ route %r{\A/features/(?<feature_name>.*)/boolean/?\Z}
10
12
 
11
13
  def post
12
- feature_name = Rack::Utils.unescape(path_parts[-2])
13
14
  feature = flipper[feature_name]
14
15
  feature.enable
15
16
  decorated_feature = Decorators::Feature.new(feature)
@@ -17,7 +18,6 @@ module Flipper
17
18
  end
18
19
 
19
20
  def delete
20
- feature_name = Rack::Utils.unescape(path_parts[-2])
21
21
  feature = flipper[feature_name.to_sym]
22
22
  feature.disable
23
23
  decorated_feature = Decorators::Feature.new(feature)
@@ -6,10 +6,11 @@ module Flipper
6
6
  module V1
7
7
  module Actions
8
8
  class ClearFeature < Api::Action
9
- route %r{features/[^/]*/clear/?\Z}
9
+ include FeatureNameFromRoute
10
+
11
+ route %r{\A/features/(?<feature_name>.*)/clear/?\Z}
10
12
 
11
13
  def delete
12
- feature_name = Rack::Utils.unescape(path_parts[-2])
13
14
  feature = flipper[feature_name]
14
15
  feature.clear
15
16
  json_response({}, 204)
@@ -6,7 +6,9 @@ module Flipper
6
6
  module V1
7
7
  module Actions
8
8
  class Feature < Api::Action
9
- route %r{features/[^/]*/?\Z}
9
+ include FeatureNameFromRoute
10
+
11
+ route %r{\A/features/(?<feature_name>.*)/?\Z}
10
12
 
11
13
  def get
12
14
  return json_error_response(:feature_not_found) unless feature_exists?(feature_name)
@@ -21,10 +23,6 @@ module Flipper
21
23
 
22
24
  private
23
25
 
24
- def feature_name
25
- @feature_name ||= Rack::Utils.unescape(path_parts.last)
26
- end
27
-
28
26
  def feature_exists?(feature_name)
29
27
  flipper.features.map(&:key).include?(feature_name)
30
28
  end
@@ -6,7 +6,7 @@ module Flipper
6
6
  module V1
7
7
  module Actions
8
8
  class Features < Api::Action
9
- route(/features\Z/)
9
+ route %r{\A/features/?\Z}
10
10
 
11
11
  def get
12
12
  keys = params['keys']
@@ -6,7 +6,9 @@ module Flipper
6
6
  module V1
7
7
  module Actions
8
8
  class GroupsGate < Api::Action
9
- route %r{features/[^/]*/groups/?\Z}
9
+ include FeatureNameFromRoute
10
+
11
+ route %r{\A/features/(?<feature_name>.*)/groups/?\Z}
10
12
 
11
13
  def post
12
14
  ensure_valid_params
@@ -46,10 +48,6 @@ module Flipper
46
48
  !allow_unregistered_groups?
47
49
  end
48
50
 
49
- def feature_name
50
- @feature_name ||= Rack::Utils.unescape(path_parts[-2])
51
- end
52
-
53
51
  def group_name
54
52
  @group_name ||= params['name']
55
53
  end
@@ -6,7 +6,9 @@ module Flipper
6
6
  module V1
7
7
  module Actions
8
8
  class PercentageOfActorsGate < Api::Action
9
- route %r{features/[^/]*/percentage_of_actors/?\Z}
9
+ include FeatureNameFromRoute
10
+
11
+ route %r{\A/features/(?<feature_name>.*)/percentage_of_actors/?\Z}
10
12
 
11
13
  def post
12
14
  if percentage < 0 || percentage > 100
@@ -28,10 +30,6 @@ module Flipper
28
30
 
29
31
  private
30
32
 
31
- def feature_name
32
- @feature_name ||= Rack::Utils.unescape(path_parts[-2])
33
- end
34
-
35
33
  def percentage
36
34
  @percentage ||= begin
37
35
  Integer(params['percentage'])
@@ -6,7 +6,9 @@ module Flipper
6
6
  module V1
7
7
  module Actions
8
8
  class PercentageOfTimeGate < Api::Action
9
- route %r{features/[^/]*/percentage_of_time/?\Z}
9
+ include FeatureNameFromRoute
10
+
11
+ route %r{\A/features/(?<feature_name>.*)/percentage_of_time/?\Z}
10
12
 
11
13
  def post
12
14
  if percentage < 0 || percentage > 100
@@ -29,10 +31,6 @@ module Flipper
29
31
 
30
32
  private
31
33
 
32
- def feature_name
33
- @feature_name ||= Rack::Utils.unescape(path_parts[-2])
34
- end
35
-
36
34
  def percentage
37
35
  @percentage ||= begin
38
36
  Integer(params['percentage'])
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.15.0'.freeze
2
+ VERSION = '0.16.0'.freeze
3
3
  end
@@ -42,6 +42,24 @@ RSpec.describe Flipper::Api::V1::Actions::ActorsGate do
42
42
  end
43
43
  end
44
44
 
45
+ describe 'enable feature with slash in name' do
46
+ before do
47
+ flipper[:my_feature].disable_actor(actor)
48
+ post '/features/my/feature/actors', flipper_id: actor.flipper_id
49
+ end
50
+
51
+ it 'enables feature for actor' do
52
+ expect(last_response.status).to eq(200)
53
+ expect(flipper["my/feature"].enabled?(actor)).to be_truthy
54
+ expect(flipper["my/feature"].enabled_gate_names).to eq([:actor])
55
+ end
56
+
57
+ it 'returns decorated feature with actor enabled' do
58
+ gate = json_response['gates'].find { |gate| gate['key'] == 'actors' }
59
+ expect(gate['value']).to eq(['1'])
60
+ end
61
+ end
62
+
45
63
  describe 'enable missing flipper_id parameter' do
46
64
  before do
47
65
  flipper[:my_feature].enable
@@ -20,6 +20,23 @@ RSpec.describe Flipper::Api::V1::Actions::BooleanGate do
20
20
  end
21
21
  end
22
22
 
23
+ describe 'enable feature with slash in name' do
24
+ before do
25
+ flipper["my/feature"].disable
26
+ post '/features/my/feature/boolean'
27
+ end
28
+
29
+ it 'enables feature' do
30
+ expect(last_response.status).to eq(200)
31
+ expect(flipper["my/feature"].on?).to be_truthy
32
+ end
33
+
34
+ it 'returns decorated feature with boolean gate enabled' do
35
+ boolean_gate = json_response['gates'].find { |gate| gate['key'] == 'boolean' }
36
+ expect(boolean_gate['value']).to be_truthy
37
+ end
38
+ end
39
+
23
40
  describe 'disable' do
24
41
  before do
25
42
  flipper[:my_feature].enable
@@ -23,4 +23,25 @@ RSpec.describe Flipper::Api::V1::Actions::ClearFeature do
23
23
  expect(flipper[:my_feature].off?).to be_truthy
24
24
  end
25
25
  end
26
+
27
+ describe 'clear feature with slash in name' do
28
+ before do
29
+ Flipper.register(:admins) {}
30
+ actor22 = Flipper::Actor.new('22')
31
+
32
+ feature = flipper["my/feature"]
33
+ feature.enable flipper.boolean
34
+ feature.enable flipper.group(:admins)
35
+ feature.enable flipper.actor(actor22)
36
+ feature.enable flipper.actors(25)
37
+ feature.enable flipper.time(45)
38
+
39
+ delete '/features/my/feature/clear'
40
+ end
41
+
42
+ it 'clears feature' do
43
+ expect(last_response.status).to eq(204)
44
+ expect(flipper["my/feature"].off?).to be_truthy
45
+ end
46
+ end
26
47
  end
@@ -22,11 +22,6 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
22
22
  'name' => 'boolean',
23
23
  'value' => 'true',
24
24
  },
25
- {
26
- 'key' => 'groups',
27
- 'name' => 'group',
28
- 'value' => [],
29
- },
30
25
  {
31
26
  'key' => 'actors',
32
27
  'name' => 'actor',
@@ -42,6 +37,11 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
42
37
  'name' => 'percentage_of_time',
43
38
  'value' => nil,
44
39
  },
40
+ {
41
+ 'key' => 'groups',
42
+ 'name' => 'group',
43
+ 'value' => [],
44
+ },
45
45
  ],
46
46
  }
47
47
 
@@ -66,11 +66,6 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
66
66
  'name' => 'boolean',
67
67
  'value' => nil,
68
68
  },
69
- {
70
- 'key' => 'groups',
71
- 'name' => 'group',
72
- 'value' => [],
73
- },
74
69
  {
75
70
  'key' => 'actors',
76
71
  'name' => 'actor',
@@ -86,6 +81,11 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
86
81
  'name' => 'percentage_of_time',
87
82
  'value' => nil,
88
83
  },
84
+ {
85
+ 'key' => 'groups',
86
+ 'name' => 'group',
87
+ 'value' => [],
88
+ },
89
89
  ],
90
90
  }
91
91
 
@@ -126,11 +126,50 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
126
126
  'name' => 'boolean',
127
127
  'value' => 'true',
128
128
  },
129
+ {
130
+ 'key' => 'actors',
131
+ 'name' => 'actor',
132
+ 'value' => [],
133
+ },
134
+ {
135
+ 'key' => 'percentage_of_actors',
136
+ 'name' => 'percentage_of_actors',
137
+ 'value' => nil,
138
+ },
139
+ {
140
+ 'key' => 'percentage_of_time',
141
+ 'name' => 'percentage_of_time',
142
+ 'value' => nil,
143
+ },
129
144
  {
130
145
  'key' => 'groups',
131
146
  'name' => 'group',
132
147
  'value' => [],
133
148
  },
149
+ ],
150
+ }
151
+
152
+ expect(last_response.status).to eq(200)
153
+ expect(json_response).to eq(response_body)
154
+ end
155
+ end
156
+
157
+ context 'feature with name that has slash' do
158
+ before do
159
+ flipper["my/feature"].enable
160
+ get '/features/my/feature'
161
+ end
162
+
163
+ it 'responds with correct attributes' do
164
+ response_body = {
165
+ 'key' => 'my/feature',
166
+ 'state' => 'on',
167
+ 'gates' => [
168
+ {
169
+ 'key' => 'boolean',
170
+ 'name' => 'boolean',
171
+ 'value' => 'true',
172
+ },
134
173
  {
135
174
  'key' => 'actors',
136
175
  'name' => 'actor',
@@ -146,6 +185,11 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
146
185
  'name' => 'percentage_of_time',
147
186
  'value' => nil,
148
187
  },
188
+ {
189
+ 'key' => 'groups',
190
+ 'name' => 'group',
191
+ 'value' => [],
192
+ },
149
193
  ],
150
194
  }
151
195
 
@@ -25,11 +25,6 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
25
25
  'name' => 'boolean',
26
26
  'value' => 'true',
27
27
  },
28
- {
29
- 'key' => 'groups',
30
- 'name' => 'group',
31
- 'value' => [],
32
- },
33
28
  {
34
29
  'key' => 'actors',
35
30
  'name' => 'actor',
@@ -45,6 +40,11 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
45
40
  'name' => 'percentage_of_time',
46
41
  'value' => nil,
47
42
  },
43
+ {
44
+ 'key' => 'groups',
45
+ 'name' => 'group',
46
+ 'value' => [],
47
+ },
48
48
  ],
49
49
  },
50
50
  ],
@@ -120,11 +120,6 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
120
120
  'name' => 'boolean',
121
121
  'value' => nil,
122
122
  },
123
- {
124
- 'key' => 'groups',
125
- 'name' => 'group',
126
- 'value' => [],
127
- },
128
123
  {
129
124
  'key' => 'actors',
130
125
  'name' => 'actor',
@@ -140,6 +135,11 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
140
135
  'name' => 'percentage_of_time',
141
136
  'value' => nil,
142
137
  },
138
+ {
139
+ 'key' => 'groups',
140
+ 'name' => 'group',
141
+ 'value' => [],
142
+ },
143
143
  ],
144
144
  }
145
145
  expect(json_response).to eq(expected_response)
@@ -26,6 +26,29 @@ RSpec.describe Flipper::Api::V1::Actions::GroupsGate do
26
26
  end
27
27
  end
28
28
 
29
+ describe 'enable feature with slash in name' do
30
+ before do
31
+ flipper["my/feature"].disable
32
+ Flipper.register(:admins) do |actor|
33
+ actor.respond_to?(:admin?) && actor.admin?
34
+ end
35
+ post '/features/my/feature/groups', name: 'admins'
36
+ end
37
+
38
+ it 'enables feature for group' do
39
+ person = double
40
+ allow(person).to receive(:flipper_id).and_return(1)
41
+ allow(person).to receive(:admin?).and_return(true)
42
+ expect(last_response.status).to eq(200)
43
+ expect(flipper["my/feature"].enabled?(person)).to be_truthy
44
+ end
45
+
46
+ it 'returns decorated feature with group enabled' do
47
+ group_gate = json_response['gates'].find { |m| m['name'] == 'group' }
48
+ expect(group_gate['value']).to eq(['admins'])
49
+ end
50
+ end
51
+
29
52
  describe 'enable without name params' do
30
53
  before do
31
54
  flipper[:my_feature].disable
@@ -4,6 +4,22 @@ RSpec.describe Flipper::Api::V1::Actions::PercentageOfActorsGate do
4
4
  let(:app) { build_api(flipper) }
5
5
 
6
6
  describe 'enable' do
7
+ context 'for feature with slash in name' do
8
+ before do
9
+ flipper["my/feature"].disable
10
+ post '/features/my/feature/percentage_of_actors', percentage: '10'
11
+ end
12
+
13
+ it 'enables gate for feature' do
14
+ expect(flipper["my/feature"].enabled_gate_names).to include(:percentage_of_actors)
15
+ end
16
+
17
+ it 'returns decorated feature with gate enabled for 10 percent of actors' do
18
+ gate = json_response['gates'].find { |gate| gate['name'] == 'percentage_of_actors' }
19
+ expect(gate['value']).to eq('10')
20
+ end
21
+ end
22
+
7
23
  context 'url-encoded request' do
8
24
  before do
9
25
  flipper[:my_feature].disable
@@ -19,6 +19,22 @@ RSpec.describe Flipper::Api::V1::Actions::PercentageOfTimeGate do
19
19
  end
20
20
  end
21
21
 
22
+ describe 'enable for feature with slash in name' do
23
+ before do
24
+ flipper["my/feature"].disable
25
+ post '/features/my/feature/percentage_of_time', percentage: '10'
26
+ end
27
+
28
+ it 'enables gate for feature' do
29
+ expect(flipper["my/feature"].enabled_gate_names).to include(:percentage_of_time)
30
+ end
31
+
32
+ it 'returns decorated feature with gate enabled for 5% of time' do
33
+ gate = json_response['gates'].find { |gate| gate['name'] == 'percentage_of_time' }
34
+ expect(gate['value']).to eq('10')
35
+ end
36
+ end
37
+
22
38
  describe 'disable without percentage' do
23
39
  before do
24
40
  flipper[:my_feature].enable_percentage_of_time(10)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-12 00:00:00.000000000 Z
11
+ date: 2018-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.15.0
39
+ version: 0.16.0
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.15.0
46
+ version: 0.16.0
47
47
  description: Rack middleware that provides an API for the flipper gem.
48
48
  email:
49
49
  - nunemaker@gmail.com
@@ -85,7 +85,8 @@ files:
85
85
  homepage: https://github.com/jnunemaker/flipper
86
86
  licenses:
87
87
  - MIT
88
- metadata: {}
88
+ metadata:
89
+ changelog_uri: https://github.com/jnunemaker/flipper/blob/master/Changelog.md
89
90
  post_install_message:
90
91
  rdoc_options: []
91
92
  require_paths: