zendesk-features 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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