flipper-api 0.8.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 197876b4a2596f886d53e8201fece30624ba53c0
4
+ data.tar.gz: ff6181dddff5fe04923d61d0e591650b95bc3654
5
+ SHA512:
6
+ metadata.gz: 9b4827eee0a212add12f181b1d8d83fb4da8b00d243e4d70bbaa3199cc0de4a4e4a71ea904839f5221c3a8b582f1ae1213e22745718c94d0a2de1784f14a3079
7
+ data.tar.gz: 920d154ee0c38d1394c6e3c0274b0d7cd711e32456d6824488959c2cf95caf4fe929b27490f8228219e889931ec9af622a0ad5e0ad2a5882b65be863667283a6
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/flipper/version', __FILE__)
3
+
4
+ flipper_api_files = lambda { |file|
5
+ file =~ /(flipper)[\/-]api/
6
+ }
7
+
8
+ Gem::Specification.new do |gem|
9
+ gem.authors = ["John Nunemaker"]
10
+ gem.email = ["nunemaker@gmail.com"]
11
+ gem.summary = "API for the Flipper gem"
12
+ gem.description = "Rack middleware that provides an API for the flipper gem."
13
+ gem.license = "MIT"
14
+ gem.homepage = "https://github.com/jnunemaker/flipper"
15
+ gem.files = `git ls-files`.split("\n").select(&flipper_api_files) + ["lib/flipper/version.rb"]
16
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n").select(&flipper_api_files)
17
+ gem.name = "flipper-api"
18
+ gem.require_paths = ["lib"]
19
+ gem.version = Flipper::VERSION
20
+
21
+ gem.add_dependency 'rack', '>= 1.4', '< 3'
22
+ gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
23
+ end
@@ -0,0 +1,116 @@
1
+ module Flipper
2
+ module Api
3
+ class Action
4
+ extend Forwardable
5
+
6
+ # Public: Call this in subclasses so the action knows its route.
7
+ #
8
+ # regex - The Regexp that this action should run for.
9
+ #
10
+ # Returns nothing.
11
+ def self.route(regex)
12
+ @regex = regex
13
+ end
14
+
15
+ # Internal: Initializes and runs an action for a given request.
16
+ #
17
+ # flipper - The Flipper::DSL instance.
18
+ # request - The Rack::Request that was sent.
19
+ #
20
+ # Returns result of Action#run.
21
+ def self.run(flipper, request)
22
+ new(flipper, request).run
23
+ end
24
+
25
+ # Internal: The regex that matches which routes this action will work for.
26
+ def self.regex
27
+ @regex || raise("#{name}.route is not set")
28
+ end
29
+
30
+ # Public: The instance of the Flipper::DSL the middleware was
31
+ # initialized with.
32
+ attr_reader :flipper
33
+
34
+ # Public: The Rack::Request to provide a response for.
35
+ attr_reader :request
36
+
37
+ # Public: The params for the request.
38
+ def_delegator :@request, :params
39
+
40
+ def initialize(flipper, request)
41
+ @flipper, @request = flipper, request
42
+ @code = 200
43
+ @headers = {"Content-Type" => Api::CONTENT_TYPE }
44
+ end
45
+
46
+ # Public: Runs the request method for the provided request.
47
+ #
48
+ # Returns whatever the request method returns in the action.
49
+ def run
50
+ if respond_to?(request_method_name)
51
+ catch(:halt) { send(request_method_name) }
52
+ else
53
+ raise Api::RequestMethodNotSupported, "#{self.class} does not support request method #{request_method_name.inspect}"
54
+ end
55
+ end
56
+
57
+ # Public: Runs another action from within the request method of a
58
+ # different action.
59
+ #
60
+ # action_class - The class of the other action to run.
61
+ #
62
+ # Examples
63
+ #
64
+ # run_other_action Home
65
+ # # => result of running Home action
66
+ #
67
+ # Returns result of other action.
68
+ def run_other_action(action_class)
69
+ action_class.new(flipper, request).run
70
+ end
71
+
72
+ # Public: Call this with a response to immediately stop the current action
73
+ # and respond however you want.
74
+ #
75
+ # response - The response you would like to return.
76
+ def halt(response)
77
+ throw :halt, response
78
+ end
79
+
80
+ def json_response(object, status = 200)
81
+ header 'Content-Type', Api::CONTENT_TYPE
82
+ status(status)
83
+ body = JSON.dump(object)
84
+ halt [@code, @headers, [body]]
85
+ end
86
+
87
+ # Public: Set the status code for the response.
88
+ #
89
+ # code - The Integer code you would like the response to return.
90
+ def status(code)
91
+ @code = code.to_i
92
+ end
93
+
94
+ # Public: Set a header.
95
+ #
96
+ # name - The String name of the header.
97
+ # value - The value of the header.
98
+ def header(name, value)
99
+ @headers[name] = value
100
+ end
101
+
102
+ private
103
+
104
+ # Private: Returns the request method converted to an action method.
105
+ def request_method_name
106
+ @request_method_name ||= @request.request_method.downcase
107
+ end
108
+
109
+ # Private: split request path by "/"
110
+ # Example: "api/v1/features/feature_name" => ['api', 'v1', 'features', 'feature_name']
111
+ def path_parts
112
+ @request.path.split("/")
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,19 @@
1
+ module Flipper
2
+ module Api
3
+ class ActionCollection
4
+ def initialize
5
+ @action_classes = []
6
+ end
7
+
8
+ def add(action_class)
9
+ @action_classes << action_class
10
+ end
11
+
12
+ def action_for_request(request)
13
+ @action_classes.detect { |action_class|
14
+ request.path_info =~ action_class.regex
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ module Flipper
2
+ module Api
3
+ # All flipper api errors inherit from this.
4
+ Error = Class.new(StandardError)
5
+
6
+ # Raised when a request method (get, post, etc.) is called for an action
7
+ # that does not know how to handle it.
8
+ RequestMethodNotSupported = Class.new(Error)
9
+ end
10
+ end
@@ -0,0 +1,62 @@
1
+ require 'rack'
2
+ require 'flipper/api/action_collection'
3
+
4
+ # Require all V1 actions automatically.
5
+ Pathname(__FILE__).dirname.join('v1/actions').each_child(false) do |name|
6
+ require "flipper/api/v1/actions/#{name}"
7
+ end
8
+
9
+ module Flipper
10
+ module Api
11
+ class Middleware
12
+ # Public: Initializes an instance of the API middleware.
13
+ #
14
+ # app - The app this middleware is included in.
15
+ # flipper_or_block - The Flipper::DSL instance or a block that yields a
16
+ # Flipper::DSL instance to use for all operations.
17
+ #
18
+ # Examples
19
+ #
20
+ # flipper = Flipper.new(...)
21
+ #
22
+ # # using with a normal flipper instance
23
+ # use Flipper::Api::Middleware, flipper
24
+ #
25
+ # # using with a block that yields a flipper instance
26
+ # use Flipper::Api::Middleware, lambda { Flipper.new(...) }
27
+ #
28
+ def initialize(app, flipper_or_block)
29
+ @app = app
30
+
31
+ if flipper_or_block.respond_to?(:call)
32
+ @flipper_block = flipper_or_block
33
+ else
34
+ @flipper = flipper_or_block
35
+ end
36
+
37
+ @action_collection = ActionCollection.new
38
+ @action_collection.add Api::V1::Actions::BooleanGate
39
+ @action_collection.add Api::V1::Actions::Feature
40
+ end
41
+
42
+ def flipper
43
+ @flipper ||= @flipper_block.call
44
+ end
45
+
46
+ def call(env)
47
+ dup.call!(env)
48
+ end
49
+
50
+ def call!(env)
51
+ request = Rack::Request.new(env)
52
+ action_class = @action_collection.action_for_request(request)
53
+ if action_class.nil?
54
+ @app.status = 404
55
+ @app.call(env)
56
+ else
57
+ action_class.run(flipper, request)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,21 @@
1
+ require 'flipper/api/action'
2
+
3
+ module Flipper
4
+ module Api
5
+ module V1
6
+ module Actions
7
+ class BooleanGate < Api::Action
8
+ route %r{api/v1/features/[^/]*/(enable|disable)/?\Z}
9
+
10
+ def put
11
+ feature_name = Rack::Utils.unescape(path_parts[-2])
12
+ feature = flipper[feature_name.to_sym]
13
+ action = Rack::Utils.unescape(path_parts.last)
14
+ feature.send(action)
15
+ json_response({}, 204)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
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 Feature < Api::Action
9
+
10
+ route %r{api/v1/features/[^/]*/?\Z}
11
+
12
+ def get
13
+ if feature_names.include?(feature_name)
14
+ feature = Decorators::Feature.new(flipper[feature_name])
15
+ json_response(feature.as_json)
16
+ else
17
+ json_response({}, 404)
18
+ end
19
+ end
20
+
21
+ def delete
22
+ if feature_names.include?(feature_name)
23
+ flipper.remove(feature_name)
24
+
25
+ json_response({}, 204)
26
+ else
27
+ json_response({}, 404)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def feature_name
34
+ @feature_name ||= Rack::Utils.unescape(path_parts.last)
35
+ end
36
+
37
+ def feature_names
38
+ @feature_names ||= flipper.adapter.features
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,28 @@
1
+ require 'delegate'
2
+ require 'flipper/api/v1/decorators/gate'
3
+
4
+ module Flipper
5
+ module Api
6
+ module V1
7
+ module Decorators
8
+ class Feature < SimpleDelegator
9
+
10
+ # Public: The feature being decorated.
11
+ alias_method :feature, :__getobj__
12
+
13
+ # Public: Returns instance as hash that is ready to be json dumped.
14
+ def as_json
15
+ gate_values = feature.gate_values
16
+ {
17
+ 'key' => key,
18
+ 'state' => state.to_s,
19
+ 'gates' => gates.map { |gate|
20
+ Decorators::Gate.new(gate, gate_values[gate.key]).as_json
21
+ },
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ module Flipper
2
+ module Api
3
+ module V1
4
+ module Decorators
5
+ class Gate < SimpleDelegator
6
+ # Public the gate being decorated
7
+ alias_method :gate, :__getobj__
8
+
9
+ # Public: the value for the gate from the adapter.
10
+ attr_reader :value
11
+
12
+ def initialize(gate, value = nil)
13
+ super gate
14
+ @value = value
15
+ end
16
+
17
+ def as_json
18
+ {
19
+ 'key' => gate.key.to_s,
20
+ 'name' => gate.name.to_s,
21
+ 'value' => value_as_json,
22
+ }
23
+ end
24
+
25
+ private
26
+
27
+ # json doesn't like sets
28
+ def value_as_json
29
+ data_type == :set ? value.to_a : value
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ require 'rack'
2
+ require 'flipper'
3
+ require 'flipper/api/middleware'
4
+
5
+ module Flipper
6
+ module Api
7
+ CONTENT_TYPE = 'application/json'.freeze
8
+
9
+ def self.app(flipper)
10
+ app = App.new(200,{ 'Content-Type' => CONTENT_TYPE }, [''])
11
+ builder = Rack::Builder.new
12
+ yield builder if block_given?
13
+ builder.use Flipper::Api::Middleware, flipper
14
+ builder.run app
15
+ builder
16
+ end
17
+
18
+ class App
19
+ # Public: HTTP response code
20
+ # Use this method to update status code before responding
21
+ attr_writer :status
22
+
23
+ def initialize(status, headers, body)
24
+ @status = status
25
+ @headers = headers
26
+ @body = body
27
+ end
28
+
29
+ # Public : Rack expects object that responds to call
30
+ # env - environment hash
31
+ def call(env)
32
+ response
33
+ end
34
+
35
+ private
36
+
37
+ def response
38
+ [@status, @headers, @body]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Flipper
2
+ VERSION = "0.8.0"
3
+ end
@@ -0,0 +1 @@
1
+ require 'flipper/api'
@@ -0,0 +1,39 @@
1
+ require 'helper'
2
+
3
+ RSpec.describe Flipper::Api::V1::Actions::BooleanGate do
4
+ let(:app) { build_api(flipper) }
5
+
6
+ describe 'enable' do
7
+ before do
8
+ flipper[:my_feature].disable
9
+ put '/api/v1/features/my_feature/enable'
10
+ end
11
+
12
+ it 'enables feature' do
13
+ expect(last_response.status).to eq(204)
14
+ expect(flipper[:my_feature].on?).to be_truthy
15
+ end
16
+ end
17
+
18
+ describe 'disable' do
19
+ before do
20
+ flipper[:my_feature].enable
21
+ put '/api/v1/features/my_feature/disable'
22
+ end
23
+
24
+ it 'disables feature' do
25
+ expect(last_response.status).to eq(204)
26
+ expect(flipper[:my_feature].off?).to be_truthy
27
+ end
28
+ end
29
+
30
+ describe 'invalid paremeter' do
31
+ before do
32
+ put '/api/v1/features/my_feature/invalid_param'
33
+ end
34
+
35
+ it 'responds with 404 when not sent enable or disable parameter' do
36
+ expect(last_response.status).to eq(404)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,118 @@
1
+ require 'helper'
2
+
3
+ RSpec.describe Flipper::Api::V1::Actions::Feature do
4
+ let(:app) { build_api(flipper) }
5
+ let(:feature) { build_feature }
6
+ let(:gate) { feature.gate(:boolean) }
7
+
8
+ describe 'get' do
9
+ context 'enabled feature' do
10
+
11
+ before do
12
+ flipper[:my_feature].enable
13
+ get 'api/v1/features/my_feature'
14
+ end
15
+
16
+ it 'responds with correct attributes' do
17
+ response_body = {
18
+ 'key' => 'my_feature',
19
+ 'state' => 'on',
20
+ 'gates' => [
21
+ {
22
+ 'key' => 'boolean',
23
+ 'name' => 'boolean',
24
+ 'value' => true,
25
+ },
26
+ {
27
+ 'key' => 'groups',
28
+ 'name' => 'group',
29
+ 'value' => [],
30
+ },
31
+ {
32
+ 'key' => 'actors',
33
+ 'name' => 'actor',
34
+ 'value' => [],
35
+ },
36
+ {
37
+ 'key' => 'percentage_of_actors',
38
+ 'name' => 'percentage_of_actors',
39
+ 'value' => 0,
40
+ },
41
+ {
42
+ 'key' => 'percentage_of_time',
43
+ 'name' => 'percentage_of_time',
44
+ 'value' => 0,
45
+ }
46
+ ]
47
+ }
48
+
49
+ expect(last_response.status).to eq(200)
50
+ expect(json_response).to eq(response_body)
51
+ end
52
+ end
53
+
54
+ context 'disabled feature' do
55
+ before do
56
+ flipper[:my_feature].disable
57
+ get 'api/v1/features/my_feature'
58
+ end
59
+
60
+ it 'responds with correct attributes' do
61
+ response_body = {
62
+ 'key' => 'my_feature',
63
+ 'state' => 'off',
64
+ 'gates' => [
65
+ {
66
+ 'key' => 'boolean',
67
+ 'name' => 'boolean',
68
+ 'value' => false,
69
+ },
70
+ {
71
+ 'key'=> 'groups',
72
+ 'name'=> 'group',
73
+ 'value'=> [],
74
+ },
75
+ {
76
+ 'key' => 'actors',
77
+ 'name' => 'actor',
78
+ 'value' => [],
79
+ },
80
+ {
81
+ 'key' => 'percentage_of_actors',
82
+ 'name' => 'percentage_of_actors',
83
+ 'value'=> 0,
84
+ },
85
+ {
86
+ 'key' => 'percentage_of_time',
87
+ 'name' => 'percentage_of_time',
88
+ 'value' => 0,
89
+ }
90
+ ]
91
+ }
92
+
93
+ expect(last_response.status).to eq(200)
94
+ expect(json_response).to eq(response_body)
95
+ end
96
+ end
97
+
98
+ context 'feature does not exist' do
99
+ before do
100
+ get 'api/v1/features/not_a_feature'
101
+ end
102
+
103
+ it 'returns 404' do
104
+ expect(last_response.status).to eq(404)
105
+ end
106
+ end
107
+ end
108
+
109
+ describe 'delete' do
110
+ it 'deletes feature' do
111
+ flipper[:my_feature].enable
112
+ expect(flipper.features.map(&:key)).to include('my_feature')
113
+ delete 'api/v1/features/my_feature'
114
+ expect(last_response.status).to eq(204)
115
+ expect(flipper.features.map(&:key)).not_to include('my_feature')
116
+ end
117
+ end
118
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flipper-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - John Nunemaker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.4'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
33
+ - !ruby/object:Gem::Dependency
34
+ name: flipper
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.8.0
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.8.0
47
+ description: Rack middleware that provides an API for the flipper gem.
48
+ email:
49
+ - nunemaker@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - flipper-api.gemspec
55
+ - lib/flipper-api.rb
56
+ - lib/flipper/api.rb
57
+ - lib/flipper/api/action.rb
58
+ - lib/flipper/api/action_collection.rb
59
+ - lib/flipper/api/error.rb
60
+ - lib/flipper/api/middleware.rb
61
+ - lib/flipper/api/v1/actions/boolean_gate.rb
62
+ - lib/flipper/api/v1/actions/feature.rb
63
+ - lib/flipper/api/v1/decorators/feature.rb
64
+ - lib/flipper/api/v1/decorators/gate.rb
65
+ - lib/flipper/version.rb
66
+ - spec/flipper/api/v1/actions/boolean_gate_spec.rb
67
+ - spec/flipper/api/v1/actions/feature_spec.rb
68
+ homepage: https://github.com/jnunemaker/flipper
69
+ licenses:
70
+ - MIT
71
+ metadata: {}
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project:
88
+ rubygems_version: 2.4.5.1
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: API for the Flipper gem
92
+ test_files:
93
+ - spec/flipper/api/v1/actions/boolean_gate_spec.rb
94
+ - spec/flipper/api/v1/actions/feature_spec.rb