diff options
author | Thom May <thom@may.lt> | 2016-12-06 18:08:36 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-12-06 18:08:36 +0000 |
commit | d63ee1c58098fd876a0dbff01af86621c13f026d (patch) | |
tree | 197b756940af9fb45f13569ac4a01454d7983f65 | |
parent | f13028debfb30620812cb479a7da8f872746521b (diff) | |
parent | 7d2bee90420aad07d36bacbb374b0803681de7b7 (diff) | |
download | mixlib-config-d63ee1c58098fd876a0dbff01af86621c13f026d.tar.gz |
Merge pull request #45 from KierranM/configurable-lists
Lists and Hashes of Contexts
-rw-r--r-- | README.md | 94 | ||||
-rw-r--r-- | lib/mixlib/config.rb | 161 | ||||
-rw-r--r-- | spec/mixlib/config_spec.rb | 129 |
3 files changed, 384 insertions, 0 deletions
@@ -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 |