summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThom May <thom@may.lt>2016-12-06 18:08:36 +0000
committerGitHub <noreply@github.com>2016-12-06 18:08:36 +0000
commitd63ee1c58098fd876a0dbff01af86621c13f026d (patch)
tree197b756940af9fb45f13569ac4a01454d7983f65
parentf13028debfb30620812cb479a7da8f872746521b (diff)
parent7d2bee90420aad07d36bacbb374b0803681de7b7 (diff)
downloadmixlib-config-d63ee1c58098fd876a0dbff01af86621c13f026d.tar.gz
Merge pull request #45 from KierranM/configurable-lists
Lists and Hashes of Contexts
-rw-r--r--README.md94
-rw-r--r--lib/mixlib/config.rb161
-rw-r--r--spec/mixlib/config_spec.rb129
3 files changed, 384 insertions, 0 deletions
diff --git a/README.md b/README.md
index 6b792e4..0a4c2df 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,100 @@ You can access these variables thus:
MyConfig[:logging][:max_log_files]
```
+### Lists of Contexts
+For use cases where you need to be able to specify a list of things with identical configuration
+you can define a `context_config_list` like so:
+
+```ruby
+ require 'mixlib/config'
+
+ module MyConfig
+ extend Mixlib::Config
+
+ # The first argument is the plural word for your item, the second is the singular
+ config_context_list :apples, :apple do
+ default :species
+ default :color, 'red'
+ default :crispness, 10
+ end
+ end
+```
+
+With this definition everytime the `apple` is called within the config file it
+will create a new item that can be configured with a block like so:
+
+```ruby
+apple do
+ species 'Royal Gala'
+end
+apple do
+ species 'Granny Smith'
+ color 'green'
+end
+```
+
+You can then iterate over the defined values in code:
+
+```ruby
+MyConfig.apples.each do |apple|
+ puts "#{apple.species} are #{apple.color}"
+end
+
+# => Royal Gala are red
+# => Granny Smith are green
+```
+
+_**Note**: When using the config context lists they must use the [block style](#block-style) or [block with argument style](#block-with-argument-style)_
+
+### Hashes of Contexts
+For use cases where you need to be able to specify a list of things with identical configuration
+that are keyed to a specific value, you can define a `context_config_hash` like so:
+
+```ruby
+ require 'mixlib/config'
+
+ module MyConfig
+ extend Mixlib::Config
+
+ # The first argument is the plural word for your item, the second is the singular
+ config_context_hash :apples, :apple do
+ default :species
+ default :color, 'red'
+ default :crispness, 10
+ end
+ end
+```
+
+This can then be used in the config file like so:
+
+```ruby
+apple 'Royal Gala' do
+ species 'Royal Gala'
+end
+apple 'Granny Smith' do
+ species 'Granny Smith'
+ color 'green'
+end
+
+# You can also reopen a context to edit a value
+apple 'Royal Gala' do
+ crispness 3
+end
+```
+
+You can then iterate over the defined values in code:
+
+```ruby
+MyConfig.apples.each do |key, apple|
+ puts "#{key} => #{apple.species} are #{apple.color}"
+end
+
+# => Royal Gala => Royal Gala are red
+# => Granny Smith => Granny Smith are green
+```
+
+_**Note**: When using the config context hashes they must use the [block style](#block-style) or [block with argument style](#block-with-argument-style)_
+
## Default Values
Mixlib::Config has a powerful default value facility. In addition to being able to specify explicit default values, you can even specify Ruby code blocks that will run if the config value is not set. This can allow you to build options whose values are based on other options.
diff --git a/lib/mixlib/config.rb b/lib/mixlib/config.rb
index 8836f8f..51c2160 100644
--- a/lib/mixlib/config.rb
+++ b/lib/mixlib/config.rb
@@ -30,10 +30,14 @@ module Mixlib
class << base; attr_accessor :configuration; end
class << base; attr_accessor :configurables; end
class << base; attr_accessor :config_contexts; end
+ class << base; attr_accessor :config_context_lists; end
+ class << base; attr_accessor :config_context_hashes; end
class << base; attr_accessor :config_parent; end
base.configuration = Hash.new
base.configurables = Hash.new
base.config_contexts = Hash.new
+ base.config_context_lists = Hash.new
+ base.config_context_hashes = Hash.new
base.initialize_mixlib_config
end
@@ -172,6 +176,18 @@ module Mixlib
context_result = context.save(include_defaults)
result[key] = context_result if context_result.size != 0 || include_defaults
end
+ self.config_context_lists.each_pair do |key, meta|
+ meta[:values].each do |context|
+ context_result = context.save(include_defaults)
+ result[key] = (result[key] || []) << context_result if context_result.size != 0 || include_defaults
+ end
+ end
+ self.config_context_hashes.each_pair do |key, meta|
+ meta[:values].each_pair do |context_key, context|
+ context_result = context.save(include_defaults)
+ (result[key] ||= {})[context_key] = context_result if context_result.size != 0 || include_defaults
+ end
+ end
result
end
alias :to_hash :save
@@ -192,6 +208,26 @@ module Mixlib
config_context.reset
end
end
+ config_context_lists.each do |key, meta|
+ meta[:values] = []
+ if hash.has_key?(key)
+ hash[key].each do |val|
+ context = define_context(meta[:definition_blocks])
+ context.restore(val)
+ meta[:values] << context
+ end
+ end
+ end
+ config_context_hashes.each do |key, meta|
+ meta[:values] = {}
+ if hash.has_key?(key)
+ hash[key].each do |vkey, val|
+ context = define_context(meta[:definition_blocks])
+ context.restore(val)
+ meta[:values][vkey] = context
+ end
+ end
+ end
end
# Merge an incoming hash with our config options
@@ -332,6 +368,70 @@ module Mixlib
context
end
+ # Allows you to create a new list of config contexts where you can define new
+ # options with default values.
+ #
+ # This method allows you to open up the configurable more than once.
+ #
+ # For example:
+ #
+ # config_context_list :listeners, :listener do
+ # configurable(:url).defaults_to("http://localhost")
+ # end
+ #
+ # === Parameters
+ # symbol<Symbol>: the plural name for contexts in the list
+ # symbol<Symbol>: the singular name for contexts in the list
+ # block<Block>: a block that will be run in the context of this new config
+ # class.
+ def config_context_list(plural_symbol, singular_symbol, &block)
+ if configurables.has_key?(symbol)
+ raise ReopenedConfigurableWithConfigContextError, "Cannot redefine config value #{plural_symbol} with a config context"
+ end
+
+ unless config_context_lists.has_key?(plural_symbol)
+ config_context_lists[plural_symbol] = {
+ definition_blocks: [],
+ values: []
+ }
+ define_list_attr_accessor_methods(plural_symbol, singular_symbol)
+ end
+
+ config_context_lists[plural_symbol][:definition_blocks] << block if block_given?
+ end
+
+ # Allows you to create a new hash of config contexts where you can define new
+ # options with default values.
+ #
+ # This method allows you to open up the configurable more than once.
+ #
+ # For example:
+ #
+ # config_context_hash :listeners, :listener do
+ # configurable(:url).defaults_to("http://localhost")
+ # end
+ #
+ # === Parameters
+ # symbol<Symbol>: the plural name for contexts in the list
+ # symbol<Symbol>: the singular name for contexts in the list
+ # block<Block>: a block that will be run in the context of this new config
+ # class.
+ def config_context_hash(plural_symbol, singular_symbol, &block)
+ if configurables.has_key?(symbol)
+ raise ReopenedConfigurableWithConfigContextError, "Cannot redefine config value #{plural_symbol} with a config context"
+ end
+
+ unless config_context_hashes.has_key?(plural_symbol)
+ config_context_hashes[plural_symbol] = {
+ definition_blocks: [],
+ values: {}
+ }
+ define_hash_attr_accessor_methods(plural_symbol, singular_symbol)
+ end
+
+ config_context_hashes[plural_symbol][:definition_blocks] << block if block_given?
+ end
+
NOT_PASSED = Object.new
# Gets or sets strict mode. When strict mode is on, only values which
@@ -433,6 +533,10 @@ module Mixlib
configurables[symbol].get(self.configuration)
elsif config_contexts.has_key?(symbol)
config_contexts[symbol]
+ elsif config_context_lists.has_key?(symbol)
+ config_context_lists[symbol]
+ elsif config_context_hashes.has_key?(symbol)
+ config_context_hashes[symbol]
else
if config_strict_mode == :warn
Chef::Log.warn("Reading unsupported config value #{symbol}.")
@@ -476,5 +580,62 @@ module Mixlib
end
end
end
+
+ def define_list_attr_accessor_methods(plural_symbol, singular_symbol)
+ # When Ruby 1.8.7 is no longer supported, this stuff can be done with define_singleton_method!
+ meta = class << self; self; end
+ # Getter for list
+ meta.send :define_method, plural_symbol do
+ internal_get(plural_symbol)[:values]
+ end
+ # Adds a single new context to the list
+ meta.send :define_method, singular_symbol do |&block|
+ context_list_details = internal_get(plural_symbol)
+ new_context = define_context(context_list_details[:definition_blocks])
+ context_list_details[:values] << new_context
+ # If the block expects no arguments, then instance_eval
+ if block.arity == 0
+ new_context.instance_eval(&block)
+ else # yield to the block
+ block.yield(new_context)
+ end
+ end
+ end
+
+ def define_hash_attr_accessor_methods(plural_symbol, singular_symbol)
+ # When Ruby 1.8.7 is no longer supported, this stuff can be done with define_singleton_method!
+ meta = class << self; self; end
+ # Getter for list
+ meta.send :define_method, plural_symbol do
+ internal_get(plural_symbol)[:values]
+ end
+ # Adds a single new context to the list
+ meta.send :define_method, singular_symbol do |key, &block|
+ context_hash_details = internal_get(plural_symbol)
+ context = if context_hash_details[:values].has_key? key
+ context_hash_details[:values][key]
+ else
+ new_context = define_context(context_hash_details[:definition_blocks])
+ context_hash_details[:values][key] = new_context
+ new_context
+ end
+ # If the block expects no arguments, then instance_eval
+ if block.arity == 0
+ context.instance_eval(&block)
+ else # yield to the block
+ block.yield(context)
+ end
+ end
+ end
+
+ def define_context(definition_blocks)
+ context = Class.new
+ context.extend(::Mixlib::Config)
+ context.config_parent = self
+ definition_blocks.each do |block|
+ context.instance_eval(&block)
+ end
+ context
+ end
end
end
diff --git a/spec/mixlib/config_spec.rb b/spec/mixlib/config_spec.rb
index 48a1b00..52d0ec9 100644
--- a/spec/mixlib/config_spec.rb
+++ b/spec/mixlib/config_spec.rb
@@ -1017,4 +1017,133 @@ describe Mixlib::Config do
end).to raise_error(Mixlib::Config::ReopenedConfigContextWithConfigurableError)
end
+ describe "config context lists" do
+ let(:klass) do
+ klass = Class.new
+ klass.extend ::Mixlib::Config
+ klass.instance_eval do
+ config_context_list(:tests, :test) do
+ default :y, 20
+ end
+ end
+ klass
+ end
+ it 'defines list methods when declaring a config_context_list' do
+ expect(klass.methods).to include :test
+ expect(klass.methods).to include :tests
+ end
+
+ it 'creates a new item each time the singular list is called' do
+ klass.test do
+ y 40
+ end
+ klass.test do
+ y 50
+ end
+ expect(klass.tests.length).to be 2
+ expect(klass.tests.first.y).to be 40
+ expect(klass.tests.last.y).to be 50
+ end
+
+ it 'can save the config list' do
+ klass.test do
+ y 40
+ end
+ klass.test do
+ y 50
+ end
+ expect(klass.save).to eq({
+ tests: [
+ { y: 40 },
+ { y: 50 }
+ ]
+ })
+ end
+
+ it 'can restore the config list from a hash' do
+ hash = {
+ tests: [
+ { y: 40 },
+ { y: 50 }
+ ]
+ }
+ klass.restore(hash)
+ expect(klass.tests.length).to be 2
+ expect(klass.tests.first.y).to be 40
+ expect(klass.tests.last.y).to be 50
+ end
+ end
+
+ describe 'config context hashes' do
+ let(:klass) do
+ klass = Class.new
+ klass.extend ::Mixlib::Config
+ klass.instance_eval do
+ config_context_hash(:tests, :test) do
+ default :y, 20
+ end
+ end
+ klass
+ end
+
+ it 'defines list methods when declaring a config_context_hash' do
+ expect(klass.methods).to include :test
+ expect(klass.methods).to include :tests
+ end
+
+ context 'when called with a new key each time' do
+ it 'creates a new item each time' do
+ klass.test :one do
+ y 40
+ end
+ klass.test :two do
+ y 50
+ end
+ expect(klass.tests.length).to be 2
+ expect(klass.tests[:one].y).to be 40
+ expect(klass.tests[:two].y).to be 50
+ end
+ end
+ context 'when called with the same key' do
+ it 'modifies the existing value' do
+ klass.test :only do
+ y 40
+ end
+ klass.test :only do
+ y 50
+ end
+ expect(klass.tests.length).to be 1
+ expect(klass.tests[:only].y).to be 50
+ end
+ end
+
+ it 'can save the config hash' do
+ klass.test :one do
+ y 40
+ end
+ klass.test :two do
+ y 50
+ end
+ expect(klass.save).to eq({
+ tests: {
+ one: { y: 40 },
+ two: { y: 50 }
+ }
+ })
+ end
+
+ it 'can restore the config hash from a hash' do
+ hash = {
+ tests: {
+ one: { y: 40 },
+ two: { y: 50 }
+ }
+ }
+ klass.restore(hash)
+ expect(klass.tests.length).to be 2
+ expect(klass.tests[:one].y).to be 40
+ expect(klass.tests[:two].y).to be 50
+ end
+ end
+
end