configure_me 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.rspec +1 -1
  2. data/{README.md → README.rdoc} +7 -7
  3. data/Rakefile +1 -7
  4. data/configure_me.gemspec +3 -7
  5. data/lib/configure_me/attribute_methods.rb +88 -0
  6. data/lib/configure_me/base.rb +58 -62
  7. data/lib/configure_me/caching.rb +19 -14
  8. data/lib/configure_me/identity.rb +15 -0
  9. data/lib/configure_me/loading.rb +57 -0
  10. data/lib/configure_me/naming.rb +18 -0
  11. data/lib/configure_me/nesting.rb +30 -24
  12. data/lib/configure_me/persistence.rb +29 -0
  13. data/lib/configure_me/persisting.rb +33 -20
  14. data/lib/configure_me/setting.rb +61 -12
  15. data/lib/configure_me/validations.rb +33 -0
  16. data/lib/configure_me/version.rb +1 -1
  17. data/lib/configure_me.rb +25 -3
  18. data/lib/generators/configure_me/setup_generator.rb +21 -0
  19. data/lib/generators/configure_me/templates/initializer.rb +1 -0
  20. data/lib/generators/{templates → configure_me/templates}/migration.rb +0 -0
  21. data/lib/generators/{templates/model.rb → configure_me/templates/model.erb} +0 -0
  22. data/lib/generators/configure_me/templates/model.rb +2 -0
  23. data/spec/configure_me/attribute_methods_spec.rb +113 -0
  24. data/spec/configure_me/base_spec.rb +43 -88
  25. data/spec/configure_me/caching_spec.rb +75 -0
  26. data/spec/configure_me/identity_spec.rb +30 -0
  27. data/spec/configure_me/loading_spec.rb +84 -0
  28. data/spec/configure_me/naming_spec.rb +23 -0
  29. data/spec/configure_me/nesting_spec.rb +36 -0
  30. data/spec/configure_me/persistence_spec.rb +62 -0
  31. data/spec/configure_me/persisting_spec.rb +123 -0
  32. data/spec/configure_me/setting_spec.rb +180 -13
  33. data/spec/spec_helper.rb +6 -0
  34. data/spec/support/active_model_lint.rb +16 -0
  35. metadata +52 -81
  36. data/features/memory.feature +0 -16
  37. data/features/step_definitions/base_steps.rb +0 -33
  38. data/lib/configure_me/settings.rb +0 -101
  39. data/lib/generators/configure_me_generator.rb +0 -19
  40. data/lib/generators/templates/initializer.rb +0 -1
data/.rspec CHANGED
@@ -1,3 +1,3 @@
1
1
  --colour
2
- --format progress
2
+ --format d
3
3
  --backtrace
@@ -1,11 +1,11 @@
1
- ## ConfigureMe
1
+ = ConfigureMe
2
2
 
3
3
  A really simple gem for helping to manage dynamic application configuration.
4
4
 
5
- ### Installation
5
+ == Installation
6
6
  gem "configure_me"
7
7
 
8
- ### Usage
8
+ == Usage
9
9
  Using ConfigureMe is easy. First, derive a class from ConfigureMe::Base. Then define your settings just as you would attributes.
10
10
 
11
11
  class AppConfig < ConfigureMe::Base
@@ -31,7 +31,7 @@ Changes to the configuration are made the same way:
31
31
  # AppConfig.instance.min_password_length
32
32
  => 6
33
33
 
34
- ### Nesting
34
+ === Nesting
35
35
  You can also nest configuration classes. To nest one config under another, just call *nest_me* from inside the nested class, passing it the class you would like to nest it under. When an instance of the parent class is created, an instance of the nested class will be instantiated at the same time.
36
36
 
37
37
  class MainConfig < ConfigureMe::Base
@@ -56,7 +56,7 @@ The default behaviour when nesting is to use the name of the nested class (exclu
56
56
  # AppConfig.instance.altextension.secret_weapon
57
57
  => "throwing star"
58
58
 
59
- ## Persisting
59
+ === Persisting
60
60
  An easily editable configuration doesn't do any good without persisting it. To store our configuration settings in the database, we need to generate an ActiveRecord model to store our settings.
61
61
 
62
62
  # rails g configure_me Setting
@@ -70,7 +70,7 @@ This will create a model/migration, and an initializer to let ConfigureMe know w
70
70
 
71
71
  Now, when updating any setting, the new value will be written to the database as well as stored in memory. Settings are converted to YAML before being written, so complex values can be stored. When accessing a setting, the database is always consulted first, and if no value is stored, the default is returned or, if no default was specified, nil.
72
72
 
73
- ## Caching
73
+ === Caching
74
74
  To really crank up the performance, you can enable caching of values. If you're paying attention, the method to enable this should be obvious.
75
75
 
76
76
  class AppConfig < ConfigureMe::Base
@@ -79,7 +79,7 @@ To really crank up the performance, you can enable caching of values. If you're
79
79
 
80
80
  Now, when values are accessed, the cache will be referenced before falling back to other means. If combined with *persist_me*, this can make dealing with dynamic site configuration perform much better than a database only solution.
81
81
 
82
- ## Putting it all together
82
+ === Putting it all together
83
83
 
84
84
  Here's a complete configuration example:
85
85
 
data/Rakefile CHANGED
@@ -2,23 +2,17 @@ require 'bundler'
2
2
  Bundler::GemHelper.install_tasks
3
3
 
4
4
  require 'rake'
5
- require 'rake/rdoctask'
6
5
  require 'rspec/core/rake_task'
7
- require 'cucumber'
8
- require 'cucumber/rake/task'
9
6
 
10
7
  RSpec::Core::RakeTask.new do |t|
11
8
  t.verbose = false
12
9
  end
13
10
 
14
11
  namespace :spec do
12
+ desc 'Generate coverage information with rcov'
15
13
  RSpec::Core::RakeTask.new(:rcov) do |t|
16
14
  t.rcov = true
17
15
  t.rcov_opts = %w{--text-report --sort coverage}
18
16
  t.rcov_opts << %w{--exclude gems\/,spec\/}
19
17
  end
20
18
  end
21
-
22
- Cucumber::Rake::Task.new(:features) do |t|
23
- t.cucumber_opts = 'features --format progress'
24
- end
data/configure_me.gemspec CHANGED
@@ -15,16 +15,12 @@ Gem::Specification.new do |s|
15
15
  s.rubyforge_project = "configure_me"
16
16
 
17
17
  s.files = `git ls-files`.split("\n")
18
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.test_files = `git ls-files -- spec/*`.split("\n")
20
19
  s.require_paths = %w(lib)
21
20
 
22
- s.add_dependency 'activemodel', '~> 3.0.1'
23
- s.add_dependency 'activesupport', '~> 3.0.1'
21
+ s.add_dependency 'rails', '3.0.3'
24
22
 
25
- s.add_development_dependency 'rspec', '~> 2.0.1'
23
+ s.add_development_dependency 'rspec', '~> 2.2.0'
26
24
  s.add_development_dependency 'rcov', '~> 0.9.9'
27
25
  s.add_development_dependency 'mocha', '~> 0.9.8'
28
- s.add_development_dependency 'cucumber', '~> 0.9.4'
29
- s.add_development_dependency 'activerecord', '~> 3.0.1'
30
26
  end
@@ -0,0 +1,88 @@
1
+ module ConfigureMe
2
+ class Base
3
+ include ActiveModel::AttributeMethods
4
+ include ActiveModel::Dirty
5
+ attribute_method_suffix('', '=', '_before_type_cast')
6
+
7
+ class << self
8
+ def setting(name, *args)
9
+ class_settings[name.to_sym] = Setting.new(name.to_sym, *args)
10
+ define_attribute_methods(true)
11
+ end
12
+
13
+ def class_settings
14
+ @class_settings ||= {}
15
+ end
16
+
17
+ def define_attribute_methods(force = false)
18
+ return if class_settings.empty?
19
+ undefine_attribute_methods if force
20
+ super(class_settings.keys)
21
+ end
22
+ end
23
+ end
24
+ module AttributeMethods
25
+
26
+ def read_attribute(name)
27
+ name_sym = name.to_sym
28
+ value = attribute_before_type_cast(name)
29
+ self.class.class_settings[name_sym].convert(value)
30
+ end
31
+
32
+ def write_attribute(name, value)
33
+ name_sym = name.to_sym
34
+ make_dirty(name_sym)
35
+ temp_attributes[name_sym] = value
36
+ end
37
+
38
+ private
39
+
40
+ def temp_attributes
41
+ @temp_attributes ||= {}
42
+ end
43
+
44
+ def make_dirty(name)
45
+ self.send("#{name.to_s}_will_change!")
46
+ end
47
+
48
+ def make_clean
49
+ temp_attributes.clear
50
+ @changed_attributes.clear if defined?(@changed_attributes)
51
+ end
52
+
53
+ def attributes
54
+ attrs = {}
55
+ self.class.class_settings.keys.each do |key|
56
+ attrs[key.to_s] = attribute(key)
57
+ end
58
+ attrs
59
+ end
60
+
61
+ def attribute_before_type_cast(name)
62
+ name_sym = name.to_sym
63
+ if self.send "#{name.to_s}_changed?".to_sym
64
+ value = temp_attributes[name_sym]
65
+ else
66
+ value = read_cache(name_sym)
67
+ if value.nil?
68
+ value = read_persist(name_sym)
69
+ unless value.nil?
70
+ write_cache(name_sym, value)
71
+ end
72
+ end
73
+ if value.nil?
74
+ value = self.class.class_settings[name_sym].default
75
+ end
76
+ end
77
+ value
78
+ end
79
+
80
+ def attribute(name)
81
+ read_attribute(name)
82
+ end
83
+
84
+ def attribute=(name, value)
85
+ write_attribute(name, value)
86
+ end
87
+ end
88
+ end
@@ -1,85 +1,81 @@
1
- require 'active_model'
2
- require 'active_support/core_ext/hash/indifferent_access'
3
- require 'singleton'
4
- require 'configure_me/settings'
5
- require 'configure_me/nesting'
6
- require 'configure_me/persisting'
7
- require 'configure_me/caching'
1
+
8
2
 
9
3
  module ConfigureMe
4
+
10
5
  class Base
11
- include ActiveModel::AttributeMethods
12
- include Settings
6
+ extend ActiveModel::Callbacks
7
+ include AttributeMethods
8
+ include Caching
9
+ include Identity
10
+ include Naming
13
11
  include Nesting
12
+ include Persistence
14
13
  include Persisting
15
- include Caching
16
- include Singleton
17
- extend ActiveModel::Naming
14
+ include Validations
15
+ extend Loading
16
+ include ActiveModel::Conversion
18
17
 
19
- class << self
20
- def config_name
21
- self.name.split('::').last.gsub(/^(.*)Config$/, '\1').downcase
18
+ def persisted?
19
+ true
20
+ end
21
+
22
+ def to_key
23
+ if persisted?
24
+ key = parent_config.nil? ? [] : parent_config.to_key
25
+ key << self.config_name
26
+ key
27
+ else
28
+ nil
22
29
  end
30
+ end
23
31
 
24
- def from_hash(root, config)
25
- if root.nil?
26
- root = const_set("RootConfig", Class.new(ConfigureMe::Base))
27
- end
32
+ def to_param
33
+ if persisted?
34
+ to_key.join('-')
35
+ else
36
+ nil
37
+ end
38
+ end
28
39
 
29
- # Determine how many root keys there are.
30
- #
31
- # If more than one, create a new nested ConfigureMe::Base
32
- # subclass for each key.
33
- #
34
- # If only one, just assign the values to the root config
35
- if config.keys.length > 1
36
- config.each_pair do |key, value|
37
- if value.is_a?(Hash)
38
- klass_name = "#{key}_config".camelize
39
- c = root.const_set(klass_name, Class.new(ConfigureMe::Base))
40
- from_hash(c, value)
41
- c.send :nest_me, root
42
- else
43
- root.send :setting, key, :string, :default => value
44
- end
45
- end
46
- root
47
- else
48
- from_hash(root, config.values.first)
49
- end
40
+ class << self
41
+ def inherited(subclass)
42
+ super
43
+ configs << subclass
50
44
  end
51
45
 
52
- def load(config)
53
- case config
54
- when Hash
55
- from_hash(nil, config)
46
+ def method_missing(method_sym, *args)
47
+ if instance.respond_to?(method_sym)
48
+ instance.send(method_sym, *args)
56
49
  else
57
- raise ::ArgumentError, "ConfigureMe: Not sure how to load type [#{config.class}]"
50
+ super
58
51
  end
59
52
  end
60
53
 
61
- def method_missing(method, *args)
62
- if instance.respond_to?(method)
63
- instance.send(method, *args)
64
- else
65
- super
54
+ def respond_to?(method_sym, include_private = false)
55
+ instance.children.each_pair do |name, instance|
56
+ if name.to_s.eql?(method_sym.to_s)
57
+ return true
58
+ end
59
+ end
60
+ if class_settings.key?(method_sym)
61
+ return true
66
62
  end
63
+ super
67
64
  end
68
- end
69
65
 
70
- def parent
71
- @parent || nil
72
- end
66
+ def find_by_id(id)
67
+ configs.each do |config|
68
+ if config.config_key.eql?(id)
69
+ return config.instance
70
+ end
71
+ end
72
+ nil
73
+ end
73
74
 
74
- def parent=(parent)
75
- @parent = parent
76
- end
75
+ private
77
76
 
78
- def initialize
79
- @children = {}
80
- @settings = settings_from_class_settings
81
- self.class.nested.each do |klass|
82
- nest(klass)
77
+ def configs
78
+ @configs ||= []
83
79
  end
84
80
  end
85
81
  end
@@ -1,25 +1,30 @@
1
- require 'active_support/concern'
2
-
3
1
  module ConfigureMe
4
- module Caching
5
- extend ActiveSupport::Concern
6
-
7
- included do
8
- @caching = false
9
- end
10
-
11
- module ClassMethods
12
- def cache_me(caching_key = nil)
2
+ class Base
3
+ class << self
4
+ def cache_me
13
5
  @caching = true
14
6
  end
15
7
 
16
8
  def caching?
17
- @caching
9
+ @caching ||= false
10
+ @caching && !ConfigureMe.cache_object.nil?
18
11
  end
19
12
  end
13
+ end
20
14
 
21
- def cache_key(name)
22
- persistence_key(name)
15
+ module Caching
16
+ def read_cache(name)
17
+ if self.class.caching?
18
+ ConfigureMe.cache_object.read(self.storage_key(name))
19
+ else
20
+ nil
21
+ end
22
+ end
23
+
24
+ def write_cache(name, value)
25
+ if self.class.caching?
26
+ ConfigureMe.cache_object.write(self.storage_key(name), value)
27
+ end
23
28
  end
24
29
  end
25
30
  end
@@ -0,0 +1,15 @@
1
+ module ConfigureMe
2
+ module Identity
3
+ def config_key
4
+ to_param
5
+ end
6
+
7
+ def config_name
8
+ self.class.name.split('::').last.gsub(/^(.*)Config$/, '\1').underscore
9
+ end
10
+
11
+ def storage_key(name)
12
+ "#{config_key}-#{name.to_s}"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,57 @@
1
+ require 'yaml'
2
+
3
+ module ConfigureMe
4
+ module Loading
5
+ def from_hash(root, config)
6
+
7
+ # Determine how many root keys there are.
8
+ #
9
+ # If more than one, create a new nested ConfigureMe::Base
10
+ # subclass for each key.
11
+ #
12
+ # If only one, just assign the values to the root config
13
+ if config.keys.length > 1
14
+ if root.nil?
15
+ root = define_custom_class('RootConfig')
16
+ end
17
+ config.each_pair do |key, value|
18
+ if value.is_a?(Hash)
19
+ klass_name = "#{key}_config".camelize
20
+ c = define_custom_class("#{key}_config".camelize)
21
+ from_hash(c, value)
22
+ c.send :nest_me, root
23
+ else
24
+ root.send :setting, key, :default => value
25
+ end
26
+ end
27
+ root
28
+ else
29
+ root = define_custom_class(config.keys.first.to_s.camelize)
30
+ from_hash(root, config.values.first)
31
+ end
32
+ end
33
+
34
+ def load(*args)
35
+ case args.first
36
+ when Hash
37
+ from_hash(nil, args.first)
38
+ when String
39
+ if File.exists?(args.first)
40
+ yml = YAML::load(File.open(args.first))
41
+ from_hash(nil, yml)
42
+ else
43
+ raise ::ArgumentError, "ConfigureMe: Invalid file: #{args.first}"
44
+ end
45
+ else
46
+ raise ::ArgumentError, "ConfigureMe: Not sure how to load type [#{args.class}]"
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def define_custom_class(name)
53
+ remove_const(name.to_sym) if const_defined?(name)
54
+ const_set(name, Class.new(ConfigureMe::Base))
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,18 @@
1
+ module ConfigureMe
2
+ class Base
3
+ extend ActiveModel::Naming
4
+
5
+ class << self
6
+ def model_name
7
+ if persisting?
8
+ ConfigureMe.persistence_klass.model_name
9
+ else
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ module Naming
17
+ end
18
+ end
@@ -1,36 +1,42 @@
1
- require 'active_support/concern'
2
-
3
1
  module ConfigureMe
4
- module Nesting
5
- extend ActiveSupport::Concern
6
-
7
- included do
8
- @@nested = []
9
- end
10
-
11
- module ClassMethods
2
+ class Base
3
+ class << self
12
4
  def nest_me(klass, name = nil)
13
- klass.nest(self)
14
- @nested_name = name || klass.config_name
15
- end
16
-
17
- def nested
18
- @nested ||= []
19
- end
20
-
21
- def nest(klass)
22
- nested << klass
5
+ klass.instance.nest(self)
23
6
  end
24
7
  end
8
+ end
9
+
10
+ module Nesting
25
11
 
26
12
  def nest(klass)
27
- @children[klass.config_name] = klass.instance
28
- klass.instance.parent = self
13
+ children[klass.instance.config_name.to_sym] = klass.instance
14
+ klass.instance.parent_config = self
29
15
  self.class_eval <<-EOF, __FILE__, __LINE__
30
- def #{klass.config_name}
31
- @children['#{klass.config_name}']
16
+ def #{klass.instance.config_name}
17
+ children[:#{klass.instance.config_name.to_s}]
32
18
  end
33
19
  EOF
34
20
  end
21
+
22
+ def parent_config
23
+ @parent_config ||= nil
24
+ end
25
+
26
+ def parent_config=(parent_config)
27
+ @parent_config = parent_config
28
+ end
29
+
30
+ def children
31
+ @children ||= {}
32
+ end
33
+
34
+ def all_configs
35
+ res = [self]
36
+ children.values.each do |child|
37
+ res.concat(child.all_configs)
38
+ end
39
+ res
40
+ end
35
41
  end
36
42
  end
@@ -0,0 +1,29 @@
1
+ module ConfigureMe
2
+ class Base
3
+ extend ActiveModel::Callbacks
4
+ define_model_callbacks :save
5
+ end
6
+ module Persistence
7
+
8
+ def save(*)
9
+ run_callbacks :save do
10
+ persist_guard do
11
+ temp_attributes.each_pair do |k,v|
12
+ write_persist(k, v)
13
+ end
14
+ end
15
+ temp_attributes.each_pair do |k,v|
16
+ write_cache(k, v)
17
+ end
18
+ make_clean
19
+ end
20
+ end
21
+
22
+ def update_attributes(new_attrs)
23
+ new_attrs.each_pair do |k,v|
24
+ write_attribute(k, v)
25
+ end
26
+ save
27
+ end
28
+ end
29
+ end
@@ -1,34 +1,47 @@
1
- require 'active_support/concern'
2
-
3
1
  module ConfigureMe
4
- module Persisting
5
- extend ActiveSupport::Concern
6
-
7
- #included do
8
- # @persisting = false
9
- #end
10
-
11
- module ClassMethods
12
- def persist_me(persistence_key = nil)
13
- if ConfigureMe.persistence_klass.nil?
14
- raise ::RuntimeError, "ConfigureMe: persistence_klass is not set. Make sure you have an initializer to assign it."
15
- end
2
+ class Base
3
+ class << self
4
+ def persist_me
16
5
  @persisting = true
17
6
  end
18
7
 
19
8
  def persisting?
20
9
  @persisting ||= false
10
+ @persisting && !ConfigureMe.persistence_klass.nil?
21
11
  end
12
+ end
13
+ end
22
14
 
23
- def persistence_key
24
- self.config_name
15
+ module Persisting
16
+
17
+ def read_persist(name)
18
+ if self.class.persisting?
19
+ setting = ConfigureMe.persistence_klass.find_by_key(self.storage_key(name))
20
+
21
+ unless setting.nil?
22
+ YAML.load(setting.value)
23
+ end
24
+ else
25
+ nil
25
26
  end
26
27
  end
27
28
 
28
- def persistence_key(name)
29
- key = "#{self.class.persistence_key}_#{name.to_s}"
30
- key = @parent.persistence_key(key) unless @parent.nil?
31
- key
29
+ def write_persist(name, value)
30
+ if self.class.persisting?
31
+ setting = ConfigureMe.persistence_klass.find_or_create_by_key(self.storage_key(name))
32
+ setting.value = value.to_yaml
33
+ setting.save!
34
+ else
35
+ true
36
+ end
37
+ end
38
+
39
+ def persist_guard(&block)
40
+ if ConfigureMe.persistence_klass.respond_to?(:transaction)
41
+ ConfigureMe.persistence_klass.transaction({}, &block)
42
+ else
43
+ block.call
44
+ end
32
45
  end
33
46
  end
34
47
  end