flipper-api 1.0.0 → 1.1.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
  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