diff --git a/README.md b/README.md index 4102073a8..73a9b808e 100644 --- a/README.md +++ b/README.md @@ -523,11 +523,53 @@ simply use a string with the prefix '+ ', e.g. ``` The blank between + and the proper string 'config' is imported for the parser because numbers ``` - attr => '+ -14', + attr => '+ -14', ``` are also possible now. For numbers -= can be built, too: ``` - attr => '- -14', + attr => '- -14', +``` +Arrays can also be marked to merge with '+' or reduce by '-' as the first item of the array: +``` + attr => [ '+', item1, item2, ... ] +``` +Result: attr += [ item1, item2, ... ] +``` + attr => [ '-', item1, item2, ... ] +``` +Result: attr -= [ item1, item2, ... ] + +That all works for attributes and custom attributes! + +Finally dictionaries can be merged when a key '+' is set: +``` + attr => { + '+' => true, + 'key1' => 'val1', + } +``` +Result: +``` + attr += { + "key1" = "val1" + } +``` +If 'attr' is a custom attribute this just works since level 3 of the dictionary: +``` + vars => { + 'level1' => { + 'level2' => { + 'level3' => { + '+' => true, + ... + }, + }, + }, + }, +``` +Parsed to: +``` + vars.level1["level2"] += level3 ``` ###### What isn't supported? diff --git a/lib/puppet_x/icinga2/utils.rb b/lib/puppet_x/icinga2/utils.rb index 4090b5e15..d165a64d4 100644 --- a/lib/puppet_x/icinga2/utils.rb +++ b/lib/puppet_x/icinga2/utils.rb @@ -79,6 +79,48 @@ # # attr => '- -14', # +# Arrays can also be marked to merge with '+' or reduce by '-' as the first item of the array: +# +# attr => [ '+', item1, item2, ... ] +# +# Result: attr += [ item1, item2, ... ] +# +# attr => [ '-' item1, item2, ... ] +# +# Result: attr -= [ item1, item2, ... ] +# +# That all works for attributes and custom attributes! +# +# Finally dictionaries can be merged when a key '+' is set: +# +# attr => { +# '+' => true, +# 'key1' => 'val1', +# } +# +# Result: +# +# attr += { +# "key1" = "val1" +# } +# +# If 'attr' is a custom attribute this just works since level 3 of the dictionary: +# +# vars => { +# 'level1' => { +# 'level2' => { +# 'level3' => { +# '+' => true, +# ... +# }, +# }, +# }, +# }, +# +# Parsed to: +# +# vars.level1["level2"] += level3 +# # === What isn't supported? # # It's not currently possible to use arrays or dictionaries in a string, like @@ -175,26 +217,28 @@ def self.process_hash(attrs, indent=2, level=3, prefix=' '*indent) result = '' attrs.each do |attr, value| if value.is_a?(Hash) + op = '+' if value.delete('+') if value.empty? result += case level - when 1 then "%s%s = {}\n" % [ prefix, attribute_types(attr) ] - when 2 then "%s[\"%s\"] = {}\n" % [ prefix, attr ] - else "%s%s = {}\n" % [ prefix, attribute_types(attr) ] + when 1 then "%s%s #{op}= {}\n" % [ prefix, attribute_types(attr) ] + when 2 then "%s[\"%s\"] #{op}= {}\n" % [ prefix, attr ] + else "%s%s #{op}= {}\n" % [ prefix, attribute_types(attr) ] end else result += case level when 1 then process_hash(value, indent, 2, "%s%s" % [ prefix, attr ]) - when 2 then "%s[\"%s\"] = {\n%s%s}\n" % [ prefix, attr, process_hash(value, indent), ' ' * (indent-2) ] - else "%s%s = {\n%s%s}\n" % [ prefix, attribute_types(attr), process_hash(value, indent+2), ' ' * indent ] + when 2 then "%s[\"%s\"] #{op}= {\n%s%s}\n" % [ prefix, attr, process_hash(value, indent), ' ' * (indent-2) ] + else "%s%s #{op}= {\n%s%s}\n" % [ prefix, attribute_types(attr), process_hash(value, indent+2), ' ' * indent ] end end elsif value.is_a?(Array) + op = value.delete_at(0) if value[0] == '+' or value[0] == '-' result += case level - when 2 then "%s[\"%s\"] = [ %s]\n" % [ prefix, attribute_types(attr), process_array(value) ] - else "%s%s = [ %s]\n" % [ prefix, attribute_types(attr), process_array(value) ] + when 2 then "%s[\"%s\"] #{op}= [ %s]\n" % [ prefix, attribute_types(attr), process_array(value) ] + else "%s%s #{op}= [ %s]\n" % [ prefix, attribute_types(attr), process_array(value) ] end else - # String: attr = '+value' -> attr += 'value' + # String: attr = '+ value' -> attr += 'value' if value =~ /^([\+,-])\s+/ operator = "#{$1}=" value = value.sub(/^[\+,-]\s+/, '') @@ -232,15 +276,40 @@ def self.process_hash(attrs, indent=2, level=3, prefix=' '*indent) value.each do |x| config += "%s%s %s\n" % [ ' ' * indent, attr, parse(x) ] if x end + elsif attr == 'vars' + if value.is_a?(Hash) + # delete pair of key '+' because a merge at this point is not allowed + value.delete('+') + config += process_hash(value, indent+2, 1, "%s%s." % [ ' ' * indent, attr]) + elsif value.is_a?(Array) + value.each do |item| + if item.is_a?(String) + config += "%s%s += %s\n" % [ ' ' * indent, attr, item.sub(/^[\+,-]\s+/, '') ] + else + item.delete('+') + if item.empty? + config += "%s%s += {}\n" % [ ' ' * indent, attr] + else + config += process_hash(item, indent+2, 1, "%s%s." % [ ' ' * indent, attr]) + end + end + end + else + if value =~ /^\+\s+/ + config += "%s%s += %s\n" % [ ' ' * indent, attr, value.sub(/^\+\s+/, '') ] + end + end else if value.is_a?(Hash) - if ['vars'].include?(attr) - config += process_hash(value, indent+2, 1, "%s%s." % [ ' ' * indent, attr]) + op = '+' if value.delete('+') + unless value.empty? + config += "%s%s #{op}= {\n%s%s}\n" % [ ' ' * indent, attr, process_hash(value, indent+2), ' ' * indent ] else - config += "%s%s = {\n%s%s}\n" % [ ' ' * indent, attr, process_hash(value, indent+2), ' ' * indent ] + config += "%s%s #{op}= {}\n" % [ ' ' * indent, attr ] end elsif value.is_a?(Array) - config += "%s%s = [ %s]\n" % [ ' ' * indent, attr, process_array(value) ] + op = value.delete_at(0) if value[0] == '+' or value[0] == '-' + config += "%s%s #{op}= [ %s]\n" % [ ' ' * indent, attr, process_array(value) ] else # String: attr = '+config' -> attr += config if value =~ /^([\+,-])\s+/ diff --git a/spec/functions/attributes_spec.rb b/spec/functions/attributes_spec.rb index de40a0f93..91e5cd1d8 100644 --- a/spec/functions/attributes_spec.rb +++ b/spec/functions/attributes_spec.rb @@ -283,17 +283,53 @@ 'foo' => ['some string, connected to another. Yeah!', 'NodeName', '42', '3.141', '2.5d', 'true'] }).and_return("foo = [ \"some string, connected to another. Yeah!\", NodeName, 42, 3.141, 2.5d, true, ]\n") + # foo += [ "some string, connected to another. Yeah!", NodeName, 42, 3.141, 2d, true, ] + is_expected.to run.with_params({ + 'foo' => ['+', 'some string, connected to another. Yeah!', 'NodeName', '42', '3.141', '2.5d', 'true'] + }).and_return("foo += [ \"some string, connected to another. Yeah!\", NodeName, 42, 3.141, 2.5d, true, ]\n") + + # foo -= [ "some string, connected to another. Yeah!", NodeName, 42, 3.141, 2d, true, ] + is_expected.to run.with_params({ + 'foo' => ['-', 'some string, connected to another. Yeah!', 'NodeName', '42', '3.141', '2.5d', 'true'] + }).and_return("foo -= [ \"some string, connected to another. Yeah!\", NodeName, 42, 3.141, 2.5d, true, ]\n") + # vars.foo = [ "some string, connected to another. Yeah!", NodeName, 42, 3.141, 2d, true, ] is_expected.to run.with_params({ 'vars' => { 'foo' => ['some string, connected to another. Yeah!', 'NodeName', '42', '3.141', '2.5d', 'true'] } }).and_return("vars.foo = [ \"some string, connected to another. Yeah!\", NodeName, 42, 3.141, 2.5d, true, ]\n") + + # vars.foo += [ "some string, connected to another. Yeah!", NodeName, 42, 3.141, 2d, true, ] + is_expected.to run.with_params({ + 'vars' => { + 'foo' => ['+', 'some string, connected to another. Yeah!', 'NodeName', '42', '3.141', '2.5d', 'true'] + } + }).and_return("vars.foo += [ \"some string, connected to another. Yeah!\", NodeName, 42, 3.141, 2.5d, true, ]\n") + + # vars.foo -= [ "some string, connected to another. Yeah!", NodeName, 42, 3.141, 2d, true, ] + is_expected.to run.with_params({ + 'vars' => { + 'foo' => ['-', 'some string, connected to another. Yeah!', 'NodeName', '42', '3.141', '2.5d', 'true'] + } + }).and_return("vars.foo -= [ \"some string, connected to another. Yeah!\", NodeName, 42, 3.141, 2.5d, true, ]\n") end it 'assign a hash' do + # foo = {} + is_expected.to run.with_params({ + 'foo' => {} + }).and_return("foo = {}\n") + + # foo += {} + is_expected.to run.with_params({ + 'foo' => { + '+' => true, + } + }).and_return("foo += {}\n") + # foo = { # string = "some string, connected to another. Yeah!" # constant = NodeName @@ -306,14 +342,35 @@ 'string' => 'some string, connected to another. Yeah!', 'constant' => 'NodeName', 'numbers' => ['42', '3.141', '-42', '-3.141'], + 'merge_array' => ['+', '42', '3.141', '-42', '-3.141'], + 'time' => '2.5d', + 'bool' => 'true', + } + }).and_return("foo = {\n string = \"some string, connected to another. Yeah!\"\n constant = NodeName\n numbers = [ 42, 3.141, -42, -3.141, ]\n merge_array += [ 42, 3.141, -42, -3.141, ]\n time = 2.5d\n bool = true\n}\n") + + # foo += { + # string = "some string, connected to another. Yeah!" + # constant = NodeName + # numbers = [ 42, 3.141, -42, -3.141, ] + # time = 2.5d + # bool = true + # } + is_expected.to run.with_params({ + 'foo' => { + '+' => true, + 'string' => 'some string, connected to another. Yeah!', + 'constant' => 'NodeName', + 'numbers' => ['42', '3.141', '-42', '-3.141'], + 'merge_array' => ['+', '42', '3.141', '-42', '-3.141'], 'time' => '2.5d', - 'bool' => 'true' + 'bool' => 'true', } - }).and_return("foo = {\n string = \"some string, connected to another. Yeah!\"\n constant = NodeName\n numbers = [ 42, 3.141, -42, -3.141, ]\n time = 2.5d\n bool = true\n}\n") + }).and_return("foo += {\n string = \"some string, connected to another. Yeah!\"\n constant = NodeName\n numbers = [ 42, 3.141, -42, -3.141, ]\n merge_array += [ 42, 3.141, -42, -3.141, ]\n time = 2.5d\n bool = true\n}\n") # vars.foo["string"] = "some string, connected to another. Yeah!" # vars.foo["constant"] = NodeName # vars.foo["numbers"] = [ 42, 3.141, -42, -3.141, ] + # vars.foo["merge_array"] += [ 42, 3.141, -42, -3.141, ] # vars.foo["time"] = 2.5d # vars.foo["bool"] = true is_expected.to run.with_params({ @@ -322,65 +379,114 @@ 'string' => 'some string, connected to another. Yeah!', 'constant' => 'NodeName', 'numbers' => ['42', '3.141', '-42', '-3.141'], + 'merge_array' => ['+', '42', '3.141', '-42', '-3.141'], 'time' => '2.5d', 'bool' => 'true' } } - }).and_return("vars.foo[\"string\"] = \"some string, connected to another. Yeah!\"\nvars.foo[\"constant\"] = NodeName\nvars.foo[\"numbers\"] = [ 42, 3.141, -42, -3.141, ]\nvars.foo[\"time\"] = 2.5d\nvars.foo[\"bool\"] = true\n") + }).and_return("vars.foo[\"string\"] = \"some string, connected to another. Yeah!\"\nvars.foo[\"constant\"] = NodeName\nvars.foo[\"numbers\"] = [ 42, 3.141, -42, -3.141, ]\nvars.foo[\"merge_array\"] += [ 42, 3.141, -42, -3.141, ]\nvars.foo[\"time\"] = 2.5d\nvars.foo[\"bool\"] = true\n") end it 'assign a nested hash' do # foobar = { - # foo = { + # foo += { # string = "some string, connected to another. Yeah!" # constant = NodeName # bool = true # } + # fooz += {} # bar = { # numbers = [ 42, 3.141, -42, -3,141, ] + # merge_array += [ 42, 3.141, -42, -3,141, ] # time = 2.5d # } + # baz = {} # } is_expected.to run.with_params({ 'foobar' => { 'foo' => { + '+' => true, 'string' => 'some string, connected to another. Yeah!', 'constant' => 'NodeName', 'bool' => 'true' }, + 'fooz' => { + '+' => true, + }, 'bar' => { 'numbers' => ['42', '3.141', '-42', '-3.141'], + 'merge_array' => ['+', '42', '3.141', '-42', '-3.141'], 'time' => '2.5d' - } + }, + 'baz' => {}, } - }).and_return("foobar = {\n foo = {\n string = \"some string, connected to another. Yeah!\"\n constant = NodeName\n bool = true\n }\n bar = {\n numbers = [ 42, 3.141, -42, -3.141, ]\n time = 2.5d\n }\n}\n") + }).and_return("foobar = {\n foo += {\n string = \"some string, connected to another. Yeah!\"\n constant = NodeName\n bool = true\n }\n fooz += {}\n bar = {\n numbers = [ 42, 3.141, -42, -3.141, ]\n merge_array += [ 42, 3.141, -42, -3.141, ]\n time = 2.5d\n }\n baz = {}\n}\n") - # vars.foobar["foo"] = { + # vars.foobar["foo"] += { # string = "some string, connected to another. Yeah!" # constant = NodeName # bool = true # } + # vars.foobar["fooz"] += {} # vars.foobar["bar"] = { # numbers = [ 42, 3.141, -42, -3,141, ] + # merge_array += [ 42, 3.141, -42, -3,141, ] # time = 2.5d # } + # vars.foobar["baz"] = {} is_expected.to run.with_params({ 'vars' => { 'foobar' => { 'foo' => { + '+' => true, 'string' => 'some string, connected to another. Yeah!', 'constant' => 'NodeName', 'bool' => 'true' }, + 'fooz' => { + '+' => true, + }, 'bar' => { 'numbers' => ['42', '3.141', '-42', '-3.141'], + 'merge_array' => ['+', '42', '3.141', '-42', '-3.141'], 'time' => '2.5d' - } + }, + 'baz' => {}, } } - }).and_return("vars.foobar[\"foo\"] = {\n string = \"some string, connected to another. Yeah!\"\n constant = NodeName\n bool = true\n}\nvars.foobar[\"bar\"] = {\n numbers = [ 42, 3.141, -42, -3.141, ]\n time = 2.5d\n}\n") + }).and_return("vars.foobar[\"foo\"] += {\n string = \"some string, connected to another. Yeah!\"\n constant = NodeName\n bool = true\n}\nvars.foobar[\"fooz\"] += {}\nvars.foobar[\"bar\"] = {\n numbers = [ 42, 3.141, -42, -3.141, ]\n merge_array += [ 42, 3.141, -42, -3.141, ]\n time = 2.5d\n}\nvars.foobar[\"baz\"] = {}\n") + end + + + # vars += config1 + # vars.foo = "some string" + # vars.bar += [ 42, 3.141, -42, -3.141, ] + # vars.baz["number"] -= 42 + # vars.baz["floating"] += 3.141 + # vars += config2 + it 'assign multiple custom attributes' do + is_expected.to run.with_params({ + 'vars' => '+ config', + }).and_return("vars += config\n") + + is_expected.to run.with_params({ + 'vars' => [ + '+ config1', + {}, + { + 'foo' => 'some string', + 'bar' => [ '+', '42', '3.141', '-42', '-3.141' ], + 'baz' => { + '+' => true, + 'number' => '- 42', + 'floating' => '+ 3.141', + }, + }, + 'config2', + ], + }).and_return("vars += config1\nvars += {}\nvars.foo = \"some string\"\nvars.bar += [ 42, 3.141, -42, -3.141, ]\nvars.baz[\"number\"] -= 42\nvars.baz[\"floating\"] += 3.141\nvars += config2\n") end