feature 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ ## 0.2.1 (2011-11-22)
2
+
3
+ * refactored gemspec
4
+
5
+ ## 0.2.0 (2011-11-22)
6
+
7
+ * add yaml config file repository
8
+
9
+ ## 0.1.0 (2011-11-18)
10
+
11
+ * first version with basic functionality and simple feature repository
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :rubygems
2
+
3
+ gem "rake"
4
+ gem "rspec"
@@ -0,0 +1,26 @@
1
+ # Feature
2
+
3
+ Feature is a [feature toggle](http://martinfowler.com/bliki/FeatureToggle.html) library for ruby.
4
+
5
+ The feature toggle functionality has to be configured by feature repositories. A feature repository simply provides lists of active and inctive features.
6
+ With this approach Feature is higly configurable and not bound to a specific kind of configuration.
7
+
8
+ ## Installation
9
+
10
+ gem install feature
11
+
12
+ ## Example usage
13
+
14
+ require 'feature'
15
+
16
+ repo = Feature::Repository::SimpleRepository.new
17
+ repo.add_active_feature :be_nice
18
+
19
+ Feature.set_repository repo
20
+
21
+ Feature.active?(:be_nice)
22
+ # => true
23
+
24
+ Feature.with(:be_nice) do
25
+ puts "you can read this"
26
+ end
@@ -0,0 +1,10 @@
1
+ require 'rake/testtask'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |spec|
5
+ spec.pattern = 'spec/**/*_spec.rb'
6
+ spec.rspec_opts = ['--colour', '-f documentation', '--backtrace']
7
+ end
8
+
9
+ task :default => [:spec] do
10
+ end
@@ -0,0 +1,90 @@
1
+ ##
2
+ # Feature module provides all methods
3
+ # - to set a feature repository
4
+ # - to check if a feature (represented by a symbol) is acitve or inactive
5
+ # - for conditional block execution with or without a feature
6
+ # - to refresh the feature lists (request them from repository)
7
+ #
8
+ module Feature
9
+ require 'feature/repository'
10
+
11
+ @repository = nil
12
+ @active_features = nil
13
+ @inactive_features = nil
14
+
15
+ ##
16
+ # Set the feature repository
17
+ #
18
+ # @param Feature::Repository::AbstractRepository, repository
19
+ #
20
+ def self.set_repository(repository)
21
+ if !repository.is_a?(Feature::Repository::AbstractRepository)
22
+ raise ArgumentError, "given argument is not a repository"
23
+ end
24
+
25
+ @repository = repository
26
+ refresh!
27
+ end
28
+
29
+ ##
30
+ # Obtains list of active and inactive features from repository
31
+ #
32
+ def self.refresh!
33
+ @active_features = @repository.active_features
34
+ @inactive_features = @repository.inactive_features
35
+ end
36
+
37
+ ##
38
+ # Requests if feature is active
39
+ #
40
+ # @param Symbol, feature
41
+ # @return Boolean
42
+ #
43
+ def self.active?(feature)
44
+ if !@repository
45
+ raise "Feature is missing Repository for obtaining feature lists"
46
+ end
47
+
48
+ @active_features.include?(feature)
49
+ end
50
+
51
+ ##
52
+ # Requests if feature is inactive
53
+ #
54
+ # @param Symbol, feature
55
+ # @return Boolean
56
+ #
57
+ def self.inactive?(feature)
58
+ if !@repository
59
+ raise "Feature is missing Repository for obtaining feature lists"
60
+ end
61
+
62
+ @inactive_features.include?(feature)
63
+ end
64
+
65
+ ##
66
+ # Execute the given block if feature is active
67
+ #
68
+ def self.with(feature)
69
+ if !block_given?
70
+ raise ArgumentError, "no block given to #{__method__}"
71
+ end
72
+
73
+ if active?(feature)
74
+ yield
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Execute the given block if feature is inactive
80
+ #
81
+ def self.without(feature)
82
+ if !block_given?
83
+ raise ArgumentError, "no block given to #{__method__}"
84
+ end
85
+
86
+ if inactive?(feature)
87
+ yield
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,7 @@
1
+ module Feature
2
+ module Repository
3
+ require 'feature/repository/abstract_repository'
4
+ require 'feature/repository/simple_repository'
5
+ require 'feature/repository/yaml_repository'
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ module Feature
2
+ module Repository
3
+ ##
4
+ # Abstract class for subclassing and building repository classes which
5
+ # provide lists of active and inactive features
6
+ #
7
+ class AbstractRepository
8
+
9
+ ##
10
+ # Constructor
11
+ #
12
+ # Should be overridden in derived class to initialize repository with all data needed
13
+ #
14
+ def initialize
15
+ raise "abstract class #{self.class.name}!"
16
+ end
17
+
18
+ ##
19
+ # Returns list of active features
20
+ #
21
+ # @return Array<Symbol>
22
+ #
23
+ def active_features
24
+ raise "#{__method__} has to be overridden in derived class"
25
+ end
26
+
27
+ ##
28
+ # Returns list of inactive features
29
+ #
30
+ # @return Array<Symbol>
31
+ #
32
+ def inactive_features
33
+ raise "#{__method__} has to be overridden in derived class"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,86 @@
1
+ module Feature
2
+ module Repository
3
+ ##
4
+ # SimpleRepository for active and inactive feature list
5
+ # Simply add features to both lists, not config or data sources required
6
+ #
7
+ class SimpleRepository < AbstractRepository
8
+ ##
9
+ # Constructor
10
+ #
11
+ def initialize
12
+ @active_features = []
13
+ @inactive_features = []
14
+ end
15
+
16
+ ##
17
+ # Returns list of active features
18
+ #
19
+ # @return Array<Symbol>
20
+ #
21
+ def active_features
22
+ @active_features.dup
23
+ end
24
+
25
+ ##
26
+ # Returns list of inactive features
27
+ #
28
+ # @return Array<Symbol>
29
+ #
30
+ def inactive_features
31
+ @inactive_features.dup
32
+ end
33
+
34
+ ##
35
+ # Add an active feature
36
+ #
37
+ # @param Sybmol, feature
38
+ #
39
+ def add_active_feature(feature)
40
+ check_feature_is_symbol(feature)
41
+ check_feature_already_in_lists(feature)
42
+ @active_features << feature
43
+ end
44
+
45
+ ##
46
+ # Add an inactive feature
47
+ #
48
+ # @param Sybmol, feature
49
+ #
50
+ def add_inactive_feature(feature)
51
+ check_feature_is_symbol(feature)
52
+ check_feature_already_in_lists(feature)
53
+ @inactive_features << feature
54
+ end
55
+
56
+ ##
57
+ # Checks if the given feature is a symbol and raises and raises an exception if so
58
+ #
59
+ # @param Sybmol, feature
60
+ #
61
+ def check_feature_is_symbol(feature)
62
+ if !feature.is_a?(Symbol)
63
+ raise ArgumentError, "given feature #{feature} is not a symbol"
64
+ end
65
+ end
66
+ private :check_feature_is_symbol
67
+
68
+ ##
69
+ # Checks if given feature is already added to list of active or inactive features
70
+ # and raises an exception if so
71
+ #
72
+ # @param Symbol, feature
73
+ #
74
+ def check_feature_already_in_lists(feature)
75
+ if @active_features.include?(feature)
76
+ raise ArgumentError, "feature :#{feature} already added to list of active features"
77
+ end
78
+
79
+ if @inactive_features.include?(feature)
80
+ raise ArgumentError, "feature :#{feature} already added to list of inactive features"
81
+ end
82
+ end
83
+ private :check_feature_already_in_lists
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,81 @@
1
+ module Feature
2
+ module Repository
3
+ ##
4
+ # YamlRepository for active and inactive feature list
5
+ # The yaml config file should look like this:
6
+ #
7
+ # features:
8
+ # an_active_feature: true
9
+ # an_inactive_feature: false
10
+ #
11
+ class YamlRepository < AbstractRepository
12
+ require 'yaml'
13
+
14
+ ##
15
+ # Constructor
16
+ #
17
+ # @param String, yaml_file_name
18
+ #
19
+ def initialize(yaml_file_name)
20
+ @yaml_file_name = yaml_file_name
21
+ end
22
+
23
+ ##
24
+ # Returns list of active features
25
+ #
26
+ # @return Array<Symbol>
27
+ #
28
+ def active_features
29
+ get_active_features_from_file
30
+ end
31
+
32
+ ##
33
+ # Return a list of active features read from the config file
34
+ #
35
+ # @return Array<Symbol>
36
+ #
37
+ def get_active_features_from_file
38
+ features_hash = read_and_parse_file_data
39
+ features = features_hash.keys.select { |feature_key| features_hash[feature_key] }
40
+ features.sort.map(&:to_sym)
41
+ end
42
+ private :get_active_features_from_file
43
+
44
+ ##
45
+ # Returns list of inactive features
46
+ #
47
+ # @return Array<Symbol>
48
+ #
49
+ def inactive_features
50
+ get_inactive_features_from_file
51
+ end
52
+
53
+ ##
54
+ # Return a list of inactive features read from the config file
55
+ #
56
+ # @return Array<Symbol>
57
+ #
58
+ def get_inactive_features_from_file
59
+ features_hash = read_and_parse_file_data
60
+ features = features_hash.keys.select { |feature_key| !features_hash[feature_key] }
61
+ features.sort.map(&:to_sym)
62
+ end
63
+ private :get_inactive_features_from_file
64
+
65
+ ##
66
+ # Read and parses the feature config file
67
+ #
68
+ # @return Hash (with :feature => true/false meaning active/inactive)
69
+ #
70
+ def read_and_parse_file_data
71
+ raw_data = File.read(@yaml_file_name)
72
+ data = YAML.load(raw_data)
73
+ if !data.is_a?(Hash) or !data.has_key?('features')
74
+ raise ArgumentError, "content of #{@yaml_file_name} does not contain proper config"
75
+ end
76
+ data['features']
77
+ end
78
+ private :read_and_parse_file_data
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Feature::Repository::AbstractRepository do
4
+ it "should raise error on creating a new instance" do
5
+ lambda do
6
+ Feature::Repository::AbstractRepository.new
7
+ end.should raise_error("abstract class Feature::Repository::AbstractRepository!")
8
+ end
9
+ end
@@ -0,0 +1,161 @@
1
+ require 'spec_helper'
2
+
3
+ describe Feature do
4
+ context "without FeatureRepository" do
5
+ it "should raise an exception when calling active?" do
6
+ lambda do
7
+ Feature.active?(:feature_a)
8
+ end.should raise_error("Feature is missing Repository for obtaining feature lists")
9
+ end
10
+
11
+ it "should raise an exception when calling inactive?" do
12
+ lambda do
13
+ Feature.inactive?(:feature_a)
14
+ end.should raise_error("Feature is missing Repository for obtaining feature lists")
15
+ end
16
+
17
+ it "should raise an exception when calling with" do
18
+ lambda do
19
+ Feature.with(:feature_a) do
20
+ end
21
+ end.should raise_error("Feature is missing Repository for obtaining feature lists")
22
+ end
23
+
24
+ it "should raise an exception when calling without" do
25
+ lambda do
26
+ Feature.without(:feature_a) do
27
+ end
28
+ end.should raise_error("Feature is missing Repository for obtaining feature lists")
29
+ end
30
+ end
31
+
32
+ context "setting Repository" do
33
+ before(:each) do
34
+ @repository = Feature::Repository::SimpleRepository.new
35
+ Feature.set_repository @repository
36
+ end
37
+
38
+ it "should raise an exception when add repository with wrong class" do
39
+ lambda do
40
+ Feature.set_repository("not a repository")
41
+ end.should raise_error(ArgumentError, "given argument is not a repository")
42
+ end
43
+
44
+ it "should set a feature repository" do
45
+ lambda do
46
+ Feature.active?(:feature_a)
47
+ end.should_not raise_error
48
+
49
+ lambda do
50
+ Feature.inactive?(:feature_a)
51
+ end.should_not raise_error
52
+ end
53
+
54
+ it "should get active features from repository once" do
55
+ @repository.add_active_feature(:feature_a)
56
+ Feature.active?(:feature_a).should be_false
57
+ end
58
+
59
+ it "should get inactive features from repository once" do
60
+ @repository.add_inactive_feature(:feature_a)
61
+ Feature.inactive?(:feature_a).should be_false
62
+ end
63
+ end
64
+
65
+ context "refresh features" do
66
+ before(:each) do
67
+ @repository = Feature::Repository::SimpleRepository.new
68
+ Feature.set_repository @repository
69
+ end
70
+
71
+ it "should refresh active feature lists from repository" do
72
+ @repository.add_active_feature(:feature_a)
73
+ Feature.refresh!
74
+ Feature.active?(:feature_a).should be_true
75
+ end
76
+
77
+ it "should refresh inactive feature lists from repository" do
78
+ @repository.add_inactive_feature(:feature_a)
79
+ Feature.refresh!
80
+ Feature.inactive?(:feature_a).should be_true
81
+ end
82
+ end
83
+
84
+ context "request features" do
85
+ before(:each) do
86
+ repository = Feature::Repository::SimpleRepository.new
87
+ repository.add_active_feature :feature_active
88
+ repository.add_inactive_feature :feature_inactive
89
+ Feature.set_repository repository
90
+ end
91
+
92
+ it "should affirm active feature is active" do
93
+ Feature.active?(:feature_active).should be_true
94
+ end
95
+
96
+ it "should not affirm active feature is inactive" do
97
+ Feature.inactive?(:feature_active).should be_false
98
+ end
99
+
100
+ it "should affirm inactive feature is inactive" do
101
+ Feature.inactive?(:feature_inactive).should be_true
102
+ end
103
+
104
+ it "should not affirm inactive feature is active" do
105
+ Feature.active?(:feature_inactive).should be_false
106
+ end
107
+
108
+ it "should call block with active feature in active list" do
109
+ reached = false
110
+
111
+ Feature.with(:feature_active) do
112
+ reached = true
113
+ end
114
+
115
+ reached.should be_true
116
+ end
117
+
118
+ it "should not call block with active feature not in active list" do
119
+ reached = false
120
+
121
+ Feature.with(:feature_inactive) do
122
+ reached = true
123
+ end
124
+
125
+ reached.should be_false
126
+ end
127
+
128
+ it "should raise exception when no block given to with" do
129
+ lambda do
130
+ Feature.with(:feature_active)
131
+ end.should raise_error(ArgumentError, "no block given to with")
132
+ end
133
+
134
+ it "should call block without inactive feature in inactive list" do
135
+ reached = false
136
+
137
+ Feature.without(:feature_inactive) do
138
+ reached = true
139
+ end
140
+
141
+ reached.should be_true
142
+ end
143
+
144
+ it "should not call block without inactive feature in inactive list" do
145
+ reached = false
146
+
147
+ Feature.without(:feature_active) do
148
+ reached = true
149
+ end
150
+
151
+ reached.should be_false
152
+ end
153
+
154
+ it "should raise exception when no block given to without" do
155
+ lambda do
156
+ Feature.without(:feature_inactive)
157
+ end.should raise_error(ArgumentError, "no block given to without")
158
+ end
159
+
160
+ end
161
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ include Feature::Repository
4
+
5
+ describe Feature::Repository::SimpleRepository do
6
+ before(:each) do
7
+ @repository = SimpleRepository.new
8
+ end
9
+
10
+ it "should have no active features after initialization" do
11
+ @repository.active_features.should == []
12
+ end
13
+
14
+ it "should have no inactive features after initialization" do
15
+ @repository.inactive_features.should == []
16
+ end
17
+
18
+ it "should add an active feature" do
19
+ @repository.add_active_feature :feature_a
20
+ @repository.active_features.should == [:feature_a]
21
+ end
22
+
23
+ it "should raise an exception when adding not a symbol as active feature" do
24
+ lambda do
25
+ @repository.add_active_feature 'feature_a'
26
+ end.should raise_error(ArgumentError, "given feature feature_a is not a symbol")
27
+ end
28
+
29
+ it "should raise an exception when adding a active feature already added as active" do
30
+ @repository.add_active_feature :feature_a
31
+ lambda do
32
+ @repository.add_active_feature :feature_a
33
+ end.should raise_error(ArgumentError, "feature :feature_a already added to list of active features")
34
+ end
35
+
36
+ it "should raise an exception when adding a active feature already added as inactive" do
37
+ @repository.add_inactive_feature :feature_a
38
+ lambda do
39
+ @repository.add_active_feature :feature_a
40
+ end.should raise_error(ArgumentError, "feature :feature_a already added to list of inactive features")
41
+ end
42
+
43
+ it "should add an inactive feature" do
44
+ @repository.add_inactive_feature :feature_a
45
+ @repository.inactive_features.should == [:feature_a]
46
+ end
47
+
48
+ it "should raise an exception when adding not a symbol as inactive feature" do
49
+ lambda do
50
+ @repository.add_inactive_feature 'feature_a'
51
+ end.should raise_error(ArgumentError, "given feature feature_a is not a symbol")
52
+ end
53
+
54
+ it "should raise an exception when adding a inactive feature already added as inactive" do
55
+ @repository.add_inactive_feature :feature_a
56
+ lambda do
57
+ @repository.add_inactive_feature :feature_a
58
+ end.should raise_error(ArgumentError, "feature :feature_a already added to list of inactive features")
59
+ end
60
+
61
+ it "should raise an exception when adding a inactive feature already added as active" do
62
+ @repository.add_active_feature :feature_a
63
+ lambda do
64
+ @repository.add_inactive_feature :feature_a
65
+ end.should raise_error(ArgumentError, "feature :feature_a already added to list of active features")
66
+ end
67
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+
4
+ include Feature::Repository
5
+
6
+ describe Feature::Repository::YamlRepository do
7
+ context "proper config file" do
8
+ before(:each) do
9
+ @filename = Tempfile.new(['feature_config', '.yaml']).path
10
+ fp = File.new(@filename, 'w')
11
+ fp.write <<"EOF";
12
+ features:
13
+ feature_a_active: true
14
+ feature_b_active: true
15
+ feature_c_inactive: false
16
+ feature_d_inactive: false
17
+ EOF
18
+ fp.close
19
+
20
+ @repo = YamlRepository.new(@filename)
21
+ end
22
+
23
+ after(:each) do
24
+ File.delete(@filename)
25
+ end
26
+
27
+ it "should read active features from a config file" do
28
+ @repo.active_features.should == [:feature_a_active, :feature_b_active]
29
+ end
30
+
31
+ it "should read inactive features from a config file" do
32
+ @repo.inactive_features.should == [:feature_c_inactive, :feature_d_inactive]
33
+ end
34
+
35
+ context "re-read config file" do
36
+ before(:each) do
37
+ fp = File.new(@filename, 'w')
38
+ fp.write <<"EOF";
39
+ features:
40
+ feature_a_active: true
41
+ feature_c_inactive: false
42
+ EOF
43
+ fp.close
44
+ end
45
+
46
+ it "should read active features new on each request" do
47
+ @repo.active_features.should == [:feature_a_active]
48
+ end
49
+
50
+ it "should read inactive features new on each request" do
51
+ @repo.inactive_features.should == [:feature_c_inactive]
52
+ end
53
+ end
54
+ end
55
+
56
+ it "should raise exception on no file found" do
57
+ repo = YamlRepository.new("/this/file/should/not/exist")
58
+ lambda do
59
+ repo.active_features
60
+ end.should raise_error(Errno::ENOENT, "No such file or directory - /this/file/should/not/exist")
61
+ end
62
+
63
+ it "should raise exception on invalid yaml" do
64
+ @filename = Tempfile.new(['feature_config', '.yaml']).path
65
+ fp = File.new(@filename, 'w')
66
+ fp.write "this is not valid feature config"
67
+ fp.close
68
+
69
+ repo = YamlRepository.new(@filename)
70
+ lambda do
71
+ repo.active_features
72
+ end.should raise_error(ArgumentError, "content of #{@filename} does not contain proper config")
73
+ end
74
+ end
@@ -0,0 +1,9 @@
1
+ require "pathname"
2
+ require 'pp'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+
7
+ SPEC_ROOT = Pathname(__FILE__).dirname.expand_path
8
+
9
+ require SPEC_ROOT.parent + 'lib/feature'
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: feature
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 1
10
+ version: 0.2.1
11
+ platform: ruby
12
+ authors:
13
+ - Markus Gerdes
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-11-22 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description:
23
+ email: github@mgsnova.de
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - lib/feature.rb
32
+ - lib/feature/repository/simple_repository.rb
33
+ - lib/feature/repository/abstract_repository.rb
34
+ - lib/feature/repository/yaml_repository.rb
35
+ - lib/feature/repository.rb
36
+ - spec/feature/simple_repository_spec.rb
37
+ - spec/feature/yaml_repository_spec.rb
38
+ - spec/feature/abstract_repository_spec.rb
39
+ - spec/feature/feature_spec.rb
40
+ - spec/spec_helper.rb
41
+ - Rakefile
42
+ - Gemfile
43
+ - README.md
44
+ - CHANGELOG.md
45
+ has_rdoc: true
46
+ homepage: http://github.com/mgsnova/feature
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 3
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.7
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Feature Toggle library for ruby
79
+ test_files: []
80
+