zendesk-features 1.0.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.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ coverage
3
+ rdoc
4
+ pkg
5
+ test/debug.log
data/README.rdoc ADDED
@@ -0,0 +1,105 @@
1
+ = Features
2
+ Features is a nice little gem for associating application features with an active record model in a Rails app.
3
+
4
+ Developed and used on http://zendesk.com for account owners to switch different application features on and off.
5
+
6
+ == Sponsored by Zendesk - Enlightened Customer Support
7
+
8
+ == Description
9
+
10
+ If the feature set of your application grows, some customers would like to limit the features to only include what they
11
+ need. To enable your customers at Zendesk to customize their help desk to their needs, we developed this nice lille feature
12
+ management system, that we thought someone else out there might find useful.
13
+
14
+ You can define your feature set in a nice directly in the class that should be associated with features:
15
+
16
+ class Account < ActiveRecord::Base
17
+ has_features do
18
+ feature :archive
19
+ feature :ssl
20
+ end
21
+ end
22
+
23
+ Which would enable code like:
24
+
25
+ account.features.archive? # checks weather an account has the archive feature.
26
+ account.features.archive.create # adds the archive feature to the account
27
+ account.features.archive.destroy # removes the archive feature to the account
28
+ account.features?(:account, :ssl) # checks if the account has both the archive and ssl features
29
+
30
+ You can also define feature dependencies:
31
+
32
+ class Account < ActiveRecord::Base
33
+ has_features do
34
+ feature :archive
35
+ feature :premium
36
+ feature :ssl, :requires => [:premium]
37
+ end
38
+ end
39
+
40
+ Which would enable code like:
41
+
42
+ account.features.ssl.available? # returns true if all the required features of the ssl feature are met.
43
+ account.features.ssl.create # raises a Features::RequirementsError if the account doesn't have the premium feature
44
+ account.features.premium.destroy # would also destroy the ssl feature
45
+
46
+ Features can also be updated with the update_attributes and update_attributes! methods.
47
+
48
+ #assuming params include { :account => { :features => { :archive => '1', :ssl => '0' } } }
49
+ account.update_attributes(params[:account]) # would add the archive feature and remove the ssl feature
50
+
51
+ WARNING: You might want to protect some features from being updated with update_attributes. You can do this with:
52
+
53
+ class Account < ActiveRecord::Base
54
+ has_features do
55
+ feature :archive
56
+ feature :premium, :protected => true
57
+ feature :ssl, :requires => [:premium]
58
+ end
59
+ end
60
+
61
+ This would protect the premium feature from being updated with update_attributes and update_attributes! The premium feature can only be updated with account.features.premium.create and account.features.premium.destroy.
62
+
63
+ We also created a view helper to help you generate the UI for enabling and disabling features:
64
+
65
+ <% form_for(:account, :html => { :method => :put }) do |f| %>
66
+ <h3>
67
+ <%= f.feature_check_box :ssl %> Enable SSL on your account
68
+ </h3>
69
+ <% end %>
70
+
71
+ == Known issues
72
+
73
+ Let us know if you find any.
74
+
75
+ == Requirements
76
+
77
+ * ActiveRecord
78
+ * ActionPack
79
+
80
+ == LICENSE:
81
+
82
+ (The MIT License)
83
+
84
+ Copyright (c) 2009 Zendesk
85
+
86
+ Permission is hereby granted, free of charge, to any person
87
+ obtaining a copy of this software and associated documentation
88
+ files (the "Software"), to deal in the Software without
89
+ restriction, including without limitation the rights to use,
90
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
91
+ copies of the Software, and to permit persons to whom the
92
+ Software is furnished to do so, subject to the following
93
+ conditions:
94
+
95
+ The above copyright notice and this permission notice shall be
96
+ included in all copies or substantial portions of the Software.
97
+
98
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
99
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
100
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
101
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
102
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
103
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
104
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
105
+ OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "features"
8
+ gem.summary = %Q{Features is a nice little gem for associating application features with an active record model in a Rails app}
9
+ gem.email = "mick@zendesk.com"
10
+ gem.homepage = "http://github.com/zendesk/features"
11
+ gem.authors = ["Mick Staugaard", "Morten Primdahl"]
12
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
13
+ end
14
+
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/*_test.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+ rescue LoadError
34
+ task :rcov do
35
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
36
+ end
37
+ end
38
+
39
+
40
+ task :default => :test
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ if File.exist?('VERSION.yml')
45
+ config = YAML.load(File.read('VERSION.yml'))
46
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
47
+ else
48
+ version = ""
49
+ end
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "features #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
56
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/lib/features.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'features/feature'
2
+ require 'features/active_record_extension'
3
+ require 'features/features_helper'
@@ -0,0 +1,135 @@
1
+ module Features
2
+ module ActiveRecordExtension
3
+ module ClassMethods
4
+ def has_features(&block)
5
+ builder = FeatureTreeBuilder.new(self)
6
+ builder.instance_eval(&block)
7
+ builder.build
8
+
9
+ has_many :features, :class_name => 'Features::Feature', :dependent => :destroy do
10
+ def available?(feature_name)
11
+ @owner.features?(*Feature.sym_to_class(feature_name).required_features.map(&:to_sym))
12
+ end
13
+
14
+ # Define the feature query methods, given that the feature +wiffle+ is defined,
15
+ # then the methods +account.features.wiffle?+ and +account.features.wiffle!+ will
16
+ # be available
17
+ Feature::LIST.each do |f|
18
+
19
+ # The query method, does this account have a given feature? account.features.wiffle?
20
+ define_method "#{f}?" do
21
+ any? { |feature| feature.matches?(f) }
22
+ end
23
+
24
+ # The finder method which returns the feature if present, otherwise a new instance, allows
25
+ # non-destructive create and delete operations:
26
+ #
27
+ # account.features.wiffle.destroy
28
+ # account.features.wiffle.create
29
+ #
30
+ # In the latter case, a the +wiffle+ feature will only be enabled if it's not already. Be careful
31
+ # not to confuse this method with the "feature enabled" method above, ie. avoid
32
+ #
33
+ # format_c if account.feature.wiffle
34
+ #
35
+ define_method "#{f}" do
36
+ instance = detect { |feature| feature.matches?(f) }
37
+ instance ||= Feature.sym_to_class(f).new(@owner.class.name.underscore.to_sym => @owner)
38
+ end
39
+ end
40
+ end
41
+
42
+ include Features::ActiveRecordExtension::InstanceMethods
43
+ alias_method_chain :update_attributes, :features
44
+ alias_method_chain :update_attributes!, :features
45
+
46
+ end
47
+ end
48
+
49
+ module InstanceMethods
50
+ # Allows you to check for multiple features like account.features?(:suggestions, :suggestions_on_web)
51
+ def features?(*feature_names)
52
+ feature_names.all? { |feature_name| features.send("#{feature_name}?") }
53
+ end
54
+
55
+ def update_attributes_with_features(attributes)
56
+ update_feature_attributes(attributes)
57
+ update_attributes_without_features(attributes)
58
+ end
59
+
60
+ def update_attributes_with_features!(attributes)
61
+ update_feature_attributes(attributes)
62
+ update_attributes_without_features!(attributes)
63
+ end
64
+
65
+ private
66
+
67
+ def update_feature_attributes(attributes)
68
+ if attributes && feature_attributes = attributes.delete(:features)
69
+ feature_attributes.each do |feature_name, value|
70
+ feature = features.send(feature_name)
71
+ if feature.protected?
72
+ logger.warn("someone tried to mass update the protected #{feature_name} feature")
73
+ else
74
+ if value == '1' || value == true
75
+ feature.create
76
+ else
77
+ feature.destroy
78
+ end
79
+ end
80
+ end
81
+ features.reset
82
+ end
83
+ end
84
+ end
85
+
86
+ class FeatureTreeBuilder
87
+ def initialize(owner_class)
88
+ @owner_class = owner_class
89
+ @definitions = Hash.new
90
+ end
91
+
92
+ def feature(name, options = {})
93
+ raise("Feature name '#{name}' is too long. Max 28 characters please...") if name.to_s.size > 28
94
+ feature_options = options.reverse_merge({:requires => [], :dependants => [], :protected => false})
95
+ feature_options[:requires] = [*feature_options[:requires]].uniq
96
+ @definitions[name.to_sym] = feature_options
97
+ end
98
+
99
+ def resolve_dependencies
100
+ @definitions.each do |name, options|
101
+ options[:requires].each do |required_feature_name|
102
+ @definitions[required_feature_name][:dependants] << name
103
+ end
104
+ end
105
+ end
106
+
107
+ def build
108
+ resolve_dependencies
109
+ #defines the feature classes
110
+ @definitions.each do |name, options|
111
+ new_feature = Object.const_set(Feature.sym_to_name(name), Class.new(Feature))
112
+ new_feature.feature_owner = @owner_class
113
+ end
114
+
115
+ #sets the feature classes options
116
+ @definitions.each do |name, options|
117
+ new_feature = Feature.sym_to_class(name)
118
+ new_feature.protect! if options[:protected]
119
+ new_feature.required_features = options[:requires].map {|f| Feature.sym_to_class(f)}
120
+ new_feature.dependant_features = options[:dependants].map {|f| Feature.sym_to_class(f)}
121
+
122
+ Feature::LIST << name
123
+ end
124
+ end
125
+ end
126
+
127
+ def self.included(receiver)
128
+ receiver.extend ClassMethods
129
+ end
130
+ end
131
+ end
132
+
133
+ ActiveRecord::Base.class_eval do
134
+ include Features::ActiveRecordExtension
135
+ end
@@ -0,0 +1,134 @@
1
+ require 'features/active_record_extension'
2
+
3
+ module Features
4
+ class RequirementsError < StandardError
5
+ end
6
+
7
+ # If you want to add a feature, simply define a suitable symbol to Feature::LIST. If for example you
8
+ # want to define the "wiffle feature", add :wiffle and you can subsequenctly do:
9
+ #
10
+ # account.features.wiffle?
11
+ # account.features.wiffle.destroy
12
+ # account.features.wiffle.create
13
+ #
14
+ class Feature < ActiveRecord::Base
15
+ abstract_class = true
16
+
17
+ LIST = []
18
+
19
+ validate_on_create :unique_type
20
+ after_create :reset_owner_association
21
+ after_destroy :reset_owner_association
22
+ before_destroy :destroy_dependant_features
23
+
24
+ def unique_type
25
+ errors.add(:feature, :taken) if self.class.exists?(self.class.feature_owner_key => send(self.class.feature_owner_key))
26
+ end
27
+
28
+ def available?
29
+ feature_owner_instance.features.available?(to_sym)
30
+ end
31
+
32
+ def create
33
+ if new_record?
34
+ raise RequirementsError.new unless available?
35
+ super
36
+ end
37
+ self
38
+ end
39
+
40
+ def matches?(sym)
41
+ to_sym == sym.to_sym
42
+ end
43
+
44
+ def to_sym
45
+ @sym ||= Feature.class_to_sym(self.class)
46
+ end
47
+
48
+ def self.to_sym
49
+ @sym ||= Feature.class_to_sym(self)
50
+ end
51
+
52
+ def self.class_to_sym(klass)
53
+ klass.name.tableize.tableize[0..-10].to_sym
54
+ end
55
+
56
+ def self.sym_to_name(sym)
57
+ "#{sym.to_s.camelize}Feature"
58
+ end
59
+
60
+ def self.sym_to_class(sym)
61
+ sym_to_name(sym).constantize
62
+ end
63
+
64
+ def self.protected?
65
+ @protect || false
66
+ end
67
+ def protected?
68
+ self.class.protected?
69
+ end
70
+
71
+ def self.required_features
72
+ @required_features ||= []
73
+ end
74
+ def required_features
75
+ self.class.required_features
76
+ end
77
+
78
+ def self.dependant_features
79
+ @dependant_features ||= []
80
+ end
81
+ def dependant_features
82
+ self.class.dependant_features
83
+ end
84
+
85
+ private
86
+
87
+ def feature_owner_instance
88
+ send(self.class.feature_owner)
89
+ end
90
+ def update_owner_timestamp
91
+ feature_owner_instance.update_attribute(:updated_at, Time.now)
92
+ end
93
+
94
+ def reset_owner_association
95
+ feature_owner_instance.features.reload
96
+ end
97
+
98
+ def destroy_dependant_features
99
+ dependant_features.each do |dependant|
100
+ feature_owner_instance.features.send(dependant.to_sym).destroy
101
+ end
102
+ end
103
+
104
+ def self.protect!
105
+ @protect = true
106
+ end
107
+
108
+ def self.required_features=(required_features)
109
+ @required_features = required_features
110
+ end
111
+ def self.dependant_features=(dependant_features)
112
+ @dependant_features = dependant_features
113
+ end
114
+
115
+ def self.feature_owner=(owner_class)
116
+ @feature_owner_sym = owner_class.name.underscore.to_sym
117
+ belongs_to @feature_owner_sym
118
+ validates_presence_of @feature_owner_sym
119
+ if owner_class.column_names.include?('updated_at')
120
+ before_create :update_owner_timestamp
121
+ before_destroy :update_owner_timestamp
122
+ end
123
+ end
124
+
125
+ def self.feature_owner
126
+ @feature_owner_sym
127
+ end
128
+
129
+ def self.feature_owner_key
130
+ "#{@feature_owner}_id".to_sym
131
+ end
132
+ end
133
+
134
+ end
@@ -0,0 +1,18 @@
1
+ module ActionView
2
+ module Helpers
3
+ def feature_check_box(model_name, method, options = {}, checked_value = "1", unchecked_value = "0")
4
+ account = @template.instance_variable_get("@#{model_name}")
5
+ throw "feature_check_box only work on models with features" unless account.respond_to?(:features)
6
+ options[:checked] = account.features.send("#{method}?")
7
+ options[:id] ||= "#{model_name}_features_#{method}"
8
+ options[:name] = "#{model_name}[features][#{method}]"
9
+ @template.check_box(model_name, "features_#{method}", options, checked_value, unchecked_value)
10
+ end
11
+
12
+ class FormBuilder
13
+ def feature_check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
14
+ @template.feature_check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)
15
+ end
16
+ end
17
+ end
18
+ end
data/test/database.yml ADDED
@@ -0,0 +1,7 @@
1
+ test:
2
+ adapter: mysql
3
+ encoding: utf8
4
+ database: features_test
5
+ username: root
6
+ password:
7
+ socket: /tmp/mysql.sock
@@ -0,0 +1,96 @@
1
+ require 'test_helper'
2
+
3
+ class Account < ActiveRecord::Base
4
+ has_features do
5
+ feature :archive_reports, :requires => [:archive, :reports]
6
+ feature :ssl, :protected => true
7
+ feature :archive
8
+ feature :reports
9
+ end
10
+ end
11
+
12
+ class FeaturesTest < ActiveSupport::TestCase
13
+ fixtures :accounts, :features
14
+
15
+ test "that features can be created" do
16
+ a = Account.create(:name => 'name')
17
+ assert(a.features.empty?)
18
+ assert(!a.features.archive?)
19
+ assert(a.features.archive.create)
20
+ assert(a.features.size == 1)
21
+ assert(a.features.archive?)
22
+ assert(a.features.archive.id == a.features.archive.create.id)
23
+ end
24
+
25
+ test "that features can be destroyed" do
26
+ a = accounts(:account1)
27
+ assert(a.features.archive?)
28
+ assert(a.features.archive.destroy)
29
+ a.features.reload
30
+ assert(!a.features.archive?)
31
+ end
32
+
33
+ test "checks for features" do
34
+ a = accounts(:account1)
35
+ assert(a.features.archive?)
36
+ assert(a.features.ssl?)
37
+ assert(!a.features.reports?)
38
+ end
39
+
40
+ test "validates feature requirements" do
41
+ a = accounts(:account1)
42
+ assert(a.features.reports.available?)
43
+ assert(!a.features.archive_reports.available?)
44
+
45
+ assert_raises Features::RequirementsError do
46
+ a.features.archive_reports.create
47
+ end
48
+
49
+ assert(a.features.reports.create)
50
+ assert(a.features.archive_reports.create)
51
+ end
52
+
53
+ test "destroys dependant features when destroyed" do
54
+ a = accounts(:account1)
55
+ assert(a.features.reports.create)
56
+ assert(a.features.archive_reports.create)
57
+
58
+ assert(a.features.archive.destroy)
59
+ a.features.reload
60
+ assert(!a.features.archive?)
61
+ assert(!a.features.archive_reports?)
62
+ assert(a.features.reports?)
63
+ end
64
+
65
+ test "mass updating should update features" do
66
+ a = Account.create(:name => 'name')
67
+ assert(a.features.empty?)
68
+ assert(!a.features.archive?)
69
+ assert(!a.features.reports?)
70
+
71
+ a.update_attributes(:features => {:archive => '1', :reports => '1'})
72
+ assert(a.features.archive?)
73
+ assert(a.features.reports?)
74
+
75
+ a.update_attributes(:features => {:archive => '0', :reports => '0'})
76
+ assert(!a.features.archive?)
77
+ assert(!a.features.reports?)
78
+ end
79
+
80
+ test "mass updating should not update protected features" do
81
+ a = Account.create(:name => 'name')
82
+ assert(a.features.empty?)
83
+ assert(!a.features.archive?)
84
+ assert(!a.features.ssl?)
85
+
86
+ a.update_attributes(:features => {:archive => '1', :ssl => '1'})
87
+ assert(a.features.archive?)
88
+ assert(!a.features.ssl?)
89
+
90
+ assert(a.features.ssl.create)
91
+ assert(a.features.ssl?)
92
+ a.update_attributes(:features => {:archive => '0', :ssl => '0'})
93
+ assert(!a.features.archive?)
94
+ assert(a.features.ssl?)
95
+ end
96
+ end
@@ -0,0 +1,3 @@
1
+ account1:
2
+ name: My Account
3
+ id: 1
@@ -0,0 +1,7 @@
1
+ account1_archive:
2
+ account_id: 1
3
+ type: ArchiveFeature
4
+
5
+ account1_ssl:
6
+ account_id: 1
7
+ type: SslFeature
data/test/schema.rb ADDED
@@ -0,0 +1,27 @@
1
+ # This file is auto-generated from the current state of the database. Instead of editing this file,
2
+ # please use the migrations feature of Active Record to incrementally modify your database, and
3
+ # then regenerate this schema definition.
4
+ #
5
+ # Note that this schema.rb definition is the authoritative source for your database schema. If you need
6
+ # to create the application database on another system, you should be using db:schema:load, not running
7
+ # all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
8
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
9
+ #
10
+ # It's strongly recommended to check this file into your version control system.
11
+
12
+ ActiveRecord::Schema.define(:version => 1) do
13
+
14
+ create_table "features", :force => true do |t|
15
+ t.string "type"
16
+ t.integer "account_id"
17
+ t.datetime "created_at"
18
+ t.datetime "updated_at"
19
+ end
20
+
21
+ create_table "accounts", :force => true do |t|
22
+ t.string "name"
23
+ t.datetime "created_at"
24
+ t.datetime "updated_at"
25
+ end
26
+
27
+ end
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'active_support'
4
+ require 'active_record'
5
+ require 'active_record/fixtures'
6
+
7
+ ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
8
+ ActiveRecord::Base.establish_connection('test')
9
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
10
+
11
+ load(File.dirname(__FILE__) + "/schema.rb")
12
+
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+ require 'features'
15
+
16
+ class ActiveSupport::TestCase
17
+ include ActiveRecord::TestFixtures
18
+
19
+ def create_fixtures(*table_names)
20
+ if block_given?
21
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
22
+ else
23
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
24
+ end
25
+ end
26
+
27
+ # Turn off transactional fixtures if you're working with MyISAM tables in MySQL
28
+ self.use_transactional_fixtures = true
29
+
30
+ # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
31
+ self.use_instantiated_fixtures = false
32
+
33
+ # Add more helper methods to be used by all tests here...
34
+ end
35
+
36
+ ActiveSupport::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
37
+ $LOAD_PATH.unshift(ActiveSupport::TestCase.fixture_path)
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zendesk-features
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Mick Staugaard
8
+ - Morten Primdahl
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-06-01 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description:
18
+ email: mick@zendesk.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files:
24
+ - README.rdoc
25
+ files:
26
+ - .gitignore
27
+ - README.rdoc
28
+ - Rakefile
29
+ - VERSION
30
+ - lib/features.rb
31
+ - lib/features/active_record_extension.rb
32
+ - lib/features/feature.rb
33
+ - lib/features/features_helper.rb
34
+ - test/database.yml
35
+ - test/features_test.rb
36
+ - test/fixtures/accounts.yml
37
+ - test/fixtures/features.yml
38
+ - test/schema.rb
39
+ - test/test_helper.rb
40
+ has_rdoc: true
41
+ homepage: http://github.com/zendesk/features
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --charset=UTF-8
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.2.0
63
+ signing_key:
64
+ specification_version: 2
65
+ summary: Features is a nice little gem for associating application features with an active record model in a Rails app
66
+ test_files:
67
+ - test/features_test.rb
68
+ - test/schema.rb
69
+ - test/test_helper.rb