flipper-api 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 623594afd3dabf5d0f693480ec96ac479ad8313051524feab6cd04cbac97c99e
4
- data.tar.gz: 9889f7fd9762e1fa15d716f3c413c4b58a88423d48b3eecd8e2643b66fbc14c6
3
+ metadata.gz: b25c0b97a02c78eb7c67cf2f4a2471f0c1ca3d6ac07fb1199f4e9b00fce15f82
4
+ data.tar.gz: 2c00025cbdb9d13b9b38e99b8ddc79786bc2c6f540d100d7ab5c8b3bfdc68234
5
5
  SHA512:
6
- metadata.gz: e8c836f8b0d3bdd2767d7a4bb580cc2ad994fd59eb42ff9135def789ab84397cae4ee97cb8129fe71e5991d5c2095dc7c7ddc45fafd91506a199e967a367c405
7
- data.tar.gz: 618237e0fe6e60ae2283d51e40b1a79a90a9d27734ad3d4e53faa1ea58c0a44368463b1c5f998c02285b68422912899c3aa78c35a550e8ca90a80202ba93f365
6
+ metadata.gz: a2a0ab2da9e51ce266b5b25e782163891b3b3be0428642391cb1b32d81b707097c9a869583c630d737633a004b4d23fdc9e5a616202d1507bb0c1c47f3f95917
7
+ data.tar.gz: 57f01b47c7e7ca94a70222a35062e6ff58b1e0ebcc1b03f344bb8092431140feb0b7a4e106626be516f5538894b74b852ed85407f0263160a0d9bbe59aa2cc4e
@@ -19,6 +19,7 @@ module Flipper
19
19
  extend Forwardable
20
20
 
21
21
  VALID_REQUEST_METHOD_NAMES = Set.new([
22
+ 'head'.freeze,
22
23
  'get'.freeze,
23
24
  'post'.freeze,
24
25
  'put'.freeze,
@@ -115,7 +116,7 @@ module Flipper
115
116
  def json_response(object, status = 200)
116
117
  header 'content-type', Api::CONTENT_TYPE
117
118
  status(status)
118
- body = JSON.dump(object)
119
+ body = Typecast.to_json(object)
119
120
  halt [@code, @headers, [body]]
120
121
  end
121
122
 
@@ -147,8 +148,12 @@ module Flipper
147
148
  private
148
149
 
149
150
  # Private: Returns the request method converted to an action method.
151
+ # Converts head to get.
150
152
  def request_method_name
151
- @request_method_name ||= @request.request_method.downcase
153
+ @request_method_name ||= begin
154
+ name = @request.request_method.downcase
155
+ name == "head" ? "get" : name
156
+ end
152
157
  end
153
158
 
154
159
  # Private: split request path by "/"
@@ -28,6 +28,7 @@ module Flipper
28
28
  flipper_id_invalid: Error.new(4, 'Required parameter flipper_id is missing.', 422),
29
29
  name_invalid: Error.new(5, 'Required parameter name is missing.', 422),
30
30
  import_invalid: Error.new(6, 'Import invalid.', 422),
31
+ expression_invalid: Error.new(7, 'The provided expression was not valid.', 422),
31
32
  }.freeze
32
33
  end
33
34
  end
@@ -34,7 +34,8 @@ module Flipper
34
34
  # This method accomplishes similar functionality
35
35
  def update_params(env, data)
36
36
  return if data.empty?
37
- parsed_request_body = JSON.parse(data)
37
+ parsed_request_body = Typecast.from_json(data)
38
+ env["parsed_request_body".freeze] = parsed_request_body
38
39
  parsed_query_string = parse_query(env[QUERY_STRING])
39
40
  parsed_query_string.merge!(parsed_request_body)
40
41
  parameters = build_query(parsed_query_string)
@@ -14,6 +14,7 @@ module Flipper
14
14
  @env_key = options.fetch(:env_key, 'flipper')
15
15
 
16
16
  @action_collection = ActionCollection.new
17
+ @action_collection.add Api::V1::Actions::ExpressionGate
17
18
  @action_collection.add Api::V1::Actions::PercentageOfTimeGate
18
19
  @action_collection.add Api::V1::Actions::PercentageOfActorsGate
19
20
  @action_collection.add Api::V1::Actions::ActorsGate
@@ -0,0 +1,43 @@
1
+ require 'flipper/api/action'
2
+ require 'flipper/api/v1/decorators/feature'
3
+
4
+ module Flipper
5
+ module Api
6
+ module V1
7
+ module Actions
8
+ class ExpressionGate < Api::Action
9
+ include FeatureNameFromRoute
10
+
11
+ route %r{\A/features/(?<feature_name>.*)/expression/?\Z}
12
+
13
+ def post
14
+ feature = flipper[feature_name]
15
+
16
+ begin
17
+ expression = Flipper::Expression.build(expression_hash)
18
+ feature.enable_expression expression
19
+ decorated_feature = Decorators::Feature.new(feature)
20
+ json_response(decorated_feature.as_json, 200)
21
+ rescue NameError, ArgumentError => exception
22
+ json_error_response(:expression_invalid)
23
+ end
24
+ end
25
+
26
+ def delete
27
+ feature = flipper[feature_name]
28
+ feature.disable_expression
29
+
30
+ decorated_feature = Decorators::Feature.new(feature)
31
+ json_response(decorated_feature.as_json, 200)
32
+ end
33
+
34
+ private
35
+
36
+ def expression_hash
37
+ @expression_hash ||= request.env["parsed_request_body".freeze] || {}.freeze
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -25,9 +25,12 @@ module Flipper
25
25
 
26
26
  private
27
27
 
28
+ # Set of types that should be represented as Array in JSON.
29
+ JSON_ARRAY_TYPES = Set[:set].freeze
30
+
28
31
  # json doesn't like sets
29
32
  def value_as_json
30
- data_type == :set ? value.to_a : value
33
+ JSON_ARRAY_TYPES.include?(data_type) ? value.to_a : value
31
34
  end
32
35
  end
33
36
  end
data/lib/flipper/api.rb CHANGED
@@ -12,6 +12,7 @@ module Flipper
12
12
  app = ->(_) { [404, { 'content-type'.freeze => CONTENT_TYPE }, ['{}'.freeze]] }
13
13
  builder = Rack::Builder.new
14
14
  yield builder if block_given?
15
+ builder.use Rack::Head
15
16
  builder.use Flipper::Api::JsonParams
16
17
  builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
17
18
  builder.use Flipper::Api::Middleware, env_key: env_key
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end
@@ -9,6 +9,10 @@ RSpec.describe Flipper::Api::Action do
9
9
  [200, {}, 'get']
10
10
  end
11
11
 
12
+ def head
13
+ [200, {}, 'head']
14
+ end
15
+
12
16
  def post
13
17
  [200, {}, 'post']
14
18
  end
@@ -38,6 +42,12 @@ RSpec.describe Flipper::Api::Action do
38
42
  expect(action.run).to eq([200, {}, 'get'])
39
43
  end
40
44
 
45
+ it 'will run head' do
46
+ fake_request = Struct.new(:request_method, :env, :session).new('HEAD', {}, {})
47
+ action = action_subclass.new(flipper, fake_request)
48
+ expect(action.run).to eq([200, {}, 'get'])
49
+ end
50
+
41
51
  it 'will run post' do
42
52
  fake_request = Struct.new(:request_method, :env, :session).new('POST', {}, {})
43
53
  action = action_subclass.new(flipper, fake_request)
@@ -0,0 +1,157 @@
1
+ RSpec.describe Flipper::Api::V1::Actions::ExpressionGate do
2
+ let(:app) { build_api(flipper) }
3
+ let(:actor) {
4
+ Flipper::Actor.new('1', {
5
+ "plan" => "basic",
6
+ "age" => 21,
7
+ })
8
+ }
9
+ let(:expression) { Flipper.property(:plan).eq("basic") }
10
+
11
+ describe 'enable' do
12
+ before do
13
+ flipper[:my_feature].disable_expression
14
+ post '/features/my_feature/expression', JSON.dump(expression.value),
15
+ "CONTENT_TYPE" => "application/json"
16
+ end
17
+
18
+ it 'enables feature for expression' do
19
+ expect(last_response.status).to eq(200)
20
+ expect(flipper[:my_feature].enabled?(actor)).to be_truthy
21
+ expect(flipper[:my_feature].enabled_gate_names).to eq([:expression])
22
+ end
23
+
24
+ it 'returns decorated feature with expression enabled' do
25
+ gate = json_response['gates'].find { |gate| gate['key'] == 'expression' }
26
+ expect(gate['value']).to eq(expression.value)
27
+ end
28
+ end
29
+
30
+ describe 'disable' do
31
+ before do
32
+ flipper[:my_feature].enable_expression(expression)
33
+ delete '/features/my_feature/expression', JSON.dump({}),
34
+ "CONTENT_TYPE" => "application/json"
35
+ end
36
+
37
+ it 'disables expression for feature' do
38
+ expect(last_response.status).to eq(200)
39
+ expect(flipper[:my_feature].enabled?(actor)).to be_falsy
40
+ expect(flipper[:my_feature].enabled_gate_names).to be_empty
41
+ end
42
+
43
+ it 'returns decorated feature with expression gate disabled' do
44
+ gate = json_response['gates'].find { |gate| gate['key'] == 'expression' }
45
+ expect(gate['value']).to be(nil)
46
+ end
47
+ end
48
+
49
+ describe 'enable feature with slash in name' do
50
+ before do
51
+ flipper["my/feature"].disable_expression
52
+ post '/features/my/feature/expression', JSON.dump(expression.value),
53
+ "CONTENT_TYPE" => "application/json"
54
+ end
55
+
56
+ it 'enables feature for expression' do
57
+ expect(last_response.status).to eq(200)
58
+ expect(flipper["my/feature"].enabled?(actor)).to be_truthy
59
+ expect(flipper["my/feature"].enabled_gate_names).to eq([:expression])
60
+ end
61
+
62
+ it 'returns decorated feature with expression enabled' do
63
+ gate = json_response['gates'].find { |gate| gate['key'] == 'expression' }
64
+ expect(gate['value']).to eq(expression.value)
65
+ end
66
+ end
67
+
68
+ describe 'enable feature with space in name' do
69
+ before do
70
+ flipper["sp ace"].disable_expression
71
+ post '/features/sp%20ace/expression', JSON.dump(expression.value),
72
+ "CONTENT_TYPE" => "application/json"
73
+ end
74
+
75
+ it 'enables feature for expression' do
76
+ expect(last_response.status).to eq(200)
77
+ expect(flipper["sp ace"].enabled?(actor)).to be_truthy
78
+ expect(flipper["sp ace"].enabled_gate_names).to eq([:expression])
79
+ end
80
+
81
+ it 'returns decorated feature with expression enabled' do
82
+ gate = json_response['gates'].find { |gate| gate['key'] == 'expression' }
83
+ expect(gate['value']).to eq(expression.value)
84
+ end
85
+ end
86
+
87
+ describe 'enable with no data' do
88
+ before do
89
+ post '/features/my_feature/expression', "CONTENT_TYPE" => "application/json"
90
+ end
91
+
92
+ it 'returns correct error response' do
93
+ expect(last_response.status).to eq(422)
94
+ expect(json_response).to eq(api_expression_invalid_response)
95
+ end
96
+ end
97
+
98
+ describe 'enable with empty object' do
99
+ before do
100
+ data = {}
101
+ post '/features/my_feature/expression', JSON.dump(data),
102
+ "CONTENT_TYPE" => "application/json"
103
+ end
104
+
105
+ it 'returns correct error response' do
106
+ expect(last_response.status).to eq(422)
107
+ expect(json_response).to eq(api_expression_invalid_response)
108
+ end
109
+ end
110
+
111
+ describe 'enable with invalid data' do
112
+ before do
113
+ data = {"blah" => "blah"}
114
+ post '/features/my_feature/expression', JSON.dump(data),
115
+ "CONTENT_TYPE" => "application/json"
116
+ end
117
+
118
+ it 'returns correct error response' do
119
+ expect(last_response.status).to eq(422)
120
+ expect(json_response).to eq(api_expression_invalid_response)
121
+ end
122
+ end
123
+
124
+ describe 'enable missing feature' do
125
+ before do
126
+ post '/features/my_feature/expression', JSON.dump(expression.value), "CONTENT_TYPE" => "application/json"
127
+ end
128
+
129
+ it 'enables expression for feature' do
130
+ expect(last_response.status).to eq(200)
131
+ expect(flipper[:my_feature].enabled?(actor)).to be_truthy
132
+ expect(flipper[:my_feature].enabled_gate_names).to eq([:expression])
133
+ end
134
+
135
+ it 'returns decorated feature with expression enabled' do
136
+ gate = json_response['gates'].find { |gate| gate['key'] == 'expression' }
137
+ expect(gate['value']).to eq(expression.value)
138
+ end
139
+ end
140
+
141
+ describe 'disable missing feature' do
142
+ before do
143
+ delete '/features/my_feature/expression', "CONTENT_TYPE" => "application/json"
144
+ end
145
+
146
+ it 'disables expression for feature' do
147
+ expect(last_response.status).to eq(200)
148
+ expect(flipper[:my_feature].enabled?(actor)).to be_falsy
149
+ expect(flipper[:my_feature].enabled_gate_names).to be_empty
150
+ end
151
+
152
+ it 'returns decorated feature with expression gate disabled' do
153
+ gate = json_response['gates'].find { |gate| gate['key'] == 'expression' }
154
+ expect(gate['value']).to be(nil)
155
+ end
156
+ end
157
+ end
@@ -20,6 +20,11 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
20
20
  'name' => 'boolean',
21
21
  'value' => 'true',
22
22
  },
23
+ {
24
+ 'key' => 'expression',
25
+ 'name' => 'expression',
26
+ 'value' => nil,
27
+ },
23
28
  {
24
29
  'key' => 'actors',
25
30
  'name' => 'actor',
@@ -64,6 +69,11 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
64
69
  'name' => 'boolean',
65
70
  'value' => nil,
66
71
  },
72
+ {
73
+ 'key' => 'expression',
74
+ 'name' => 'expression',
75
+ 'value' => nil,
76
+ },
67
77
  {
68
78
  'key' => 'actors',
69
79
  'name' => 'actor',
@@ -124,6 +134,11 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
124
134
  'name' => 'boolean',
125
135
  'value' => 'true',
126
136
  },
137
+ {
138
+ 'key' => 'expression',
139
+ 'name' => 'expression',
140
+ 'value' => nil,
141
+ },
127
142
  {
128
143
  'key' => 'actors',
129
144
  'name' => 'actor',
@@ -168,6 +183,11 @@ RSpec.describe Flipper::Api::V1::Actions::Feature do
168
183
  'name' => 'boolean',
169
184
  'value' => 'true',
170
185
  },
186
+ {
187
+ 'key' => 'expression',
188
+ 'name' => 'expression',
189
+ 'value' => nil,
190
+ },
171
191
  {
172
192
  'key' => 'actors',
173
193
  'name' => 'actor',
@@ -24,6 +24,11 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
24
24
  'name' => 'boolean',
25
25
  'value' => 'true',
26
26
  },
27
+ {
28
+ 'key' => 'expression',
29
+ 'name' => 'expression',
30
+ 'value' => nil,
31
+ },
27
32
  {
28
33
  'key' => 'actors',
29
34
  'name' => 'actor',
@@ -60,6 +65,7 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
60
65
  'state' => 'on',
61
66
  'gates' => [
62
67
  { 'key' => 'boolean', 'value' => 'true'},
68
+ {"key" => "expression", "value" => nil},
63
69
  { 'key' => 'actors', 'value' => ['10']},
64
70
  {'key' => 'percentage_of_actors', 'value' => nil},
65
71
  { 'key' => 'percentage_of_time', 'value' => nil},
@@ -140,6 +146,11 @@ RSpec.describe Flipper::Api::V1::Actions::Features do
140
146
  'name' => 'boolean',
141
147
  'value' => nil,
142
148
  },
149
+ {
150
+ 'key' => 'expression',
151
+ 'name' => 'expression',
152
+ 'value' => nil,
153
+ },
143
154
  {
144
155
  'key' => 'actors',
145
156
  'name' => 'actor',
@@ -2,6 +2,8 @@ RSpec.describe Flipper::Api::V1::Actions::Import do
2
2
  let(:app) { build_api(flipper) }
3
3
 
4
4
  describe 'post' do
5
+ let(:expression) { Flipper.property(:plan).eq("basic") }
6
+
5
7
  context 'succesful request' do
6
8
  before do
7
9
  flipper.enable(:search)
@@ -10,6 +12,7 @@ RSpec.describe Flipper::Api::V1::Actions::Import do
10
12
  source_flipper = build_flipper
11
13
  source_flipper.disable(:search)
12
14
  source_flipper.enable_actor(:google_analytics, Flipper::Actor.new("User;1"))
15
+ source_flipper.enable(:analytics, expression)
13
16
 
14
17
  export = source_flipper.export
15
18
 
@@ -23,7 +26,8 @@ RSpec.describe Flipper::Api::V1::Actions::Import do
23
26
  it 'imports features' do
24
27
  expect(flipper[:search].boolean_value).to be(false)
25
28
  expect(flipper[:google_analytics].actors_value).to eq(Set["User;1"])
26
- expect(flipper.features.map(&:key)).to eq(["search", "google_analytics"])
29
+ expect(flipper[:analytics].expression_value).to eq(expression.value)
30
+ expect(flipper.features.map(&:key)).to eq(["search", "google_analytics", "analytics"])
27
31
  end
28
32
  end
29
33
 
@@ -14,6 +14,20 @@ RSpec.describe Flipper::Api do
14
14
  end
15
15
  end
16
16
 
17
+ describe "Head requests" do
18
+ let(:app) { build_api(flipper) }
19
+
20
+ it "are allowed but perform get and return no body" do
21
+ flipper.enable :a
22
+ flipper.disable :b
23
+
24
+ head '/features'
25
+
26
+ expect(last_response.status).to be(200)
27
+ expect(last_response.body).to be_empty
28
+ end
29
+ end
30
+
17
31
  describe 'Initializing middleware lazily with a block' do
18
32
  let(:app) { build_api(-> { flipper }) }
19
33
 
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: 1.0.0
4
+ version: 1.1.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: 2023-08-23 00:00:00.000000000 Z
11
+ date: 2023-12-08 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: 1.0.0
39
+ version: 1.1.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: 1.0.0
46
+ version: 1.1.0
47
47
  description:
48
48
  email: support@flippercloud.io
49
49
  executables: []
@@ -63,6 +63,7 @@ files:
63
63
  - lib/flipper/api/v1/actions/actors_gate.rb
64
64
  - lib/flipper/api/v1/actions/boolean_gate.rb
65
65
  - lib/flipper/api/v1/actions/clear_feature.rb
66
+ - lib/flipper/api/v1/actions/expression_gate.rb
66
67
  - lib/flipper/api/v1/actions/feature.rb
67
68
  - lib/flipper/api/v1/actions/features.rb
68
69
  - lib/flipper/api/v1/actions/groups_gate.rb
@@ -79,6 +80,7 @@ files:
79
80
  - spec/flipper/api/v1/actions/actors_spec.rb
80
81
  - spec/flipper/api/v1/actions/boolean_gate_spec.rb
81
82
  - spec/flipper/api/v1/actions/clear_feature_spec.rb
83
+ - spec/flipper/api/v1/actions/expression_gate_spec.rb
82
84
  - spec/flipper/api/v1/actions/feature_spec.rb
83
85
  - spec/flipper/api/v1/actions/features_spec.rb
84
86
  - spec/flipper/api/v1/actions/groups_gate_spec.rb
@@ -122,6 +124,7 @@ test_files:
122
124
  - spec/flipper/api/v1/actions/actors_spec.rb
123
125
  - spec/flipper/api/v1/actions/boolean_gate_spec.rb
124
126
  - spec/flipper/api/v1/actions/clear_feature_spec.rb
127
+ - spec/flipper/api/v1/actions/expression_gate_spec.rb
125
128
  - spec/flipper/api/v1/actions/feature_spec.rb
126
129
  - spec/flipper/api/v1/actions/features_spec.rb
127
130
  - spec/flipper/api/v1/actions/groups_gate_spec.rb