Fix #2389 - Enhance Puppet DSL with Hashes
This bring a new container syntax to the Puppet DSL: hashes. Hashes are defined like Ruby Hash: { key1 => val1, ... } Hash keys are strings, but hash values can be any possible right values admitted in Puppet DSL (ie function call, variables access...) Currently it is possible: 1) to assign hashes to variable $myhash = { key1 => "myval", key2 => $b } 2) to access hash members (recursively) from a variable containing a hash (works for array too): $myhash = { key => { subkey => "b" }} notice($myhash[key][subjey]] 3) to use hash member access as resource title 4) to use hash in default definition parameter or resource parameter if the type supports it (known for the moment). It is not possible to string interpolate an hash access. If it proves to be an issue it can be added or work-arounded with a string concatenation operator easily. It is not possible to use an hash as a resource title. This might be possible once we support compound resource title. Unlike the proposed syntax in the ticket it is not possible to assign individual hash member (mostly to respect write once nature of variable in puppet). Signed-off-by: Brice Figureau <brice-puppet@daysofwonder.com>
This commit is contained in:
Родитель
9122ac5128
Коммит
75c32f910e
|
@ -92,6 +92,7 @@ end
|
|||
# And include all of the AST subclasses.
|
||||
require 'puppet/parser/ast/arithmetic_operator'
|
||||
require 'puppet/parser/ast/astarray'
|
||||
require 'puppet/parser/ast/asthash'
|
||||
require 'puppet/parser/ast/branch'
|
||||
require 'puppet/parser/ast/boolean_operator'
|
||||
require 'puppet/parser/ast/caseopt'
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
require 'puppet/parser/ast/leaf'
|
||||
|
||||
class Puppet::Parser::AST
|
||||
class ASTHash < Leaf
|
||||
include Enumerable
|
||||
|
||||
def [](index)
|
||||
end
|
||||
|
||||
# Evaluate our children.
|
||||
def evaluate(scope)
|
||||
items = {}
|
||||
|
||||
@value.each_pair do |k,v|
|
||||
items.merge!({ k => v.safeevaluate(scope) })
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
def merge(hash)
|
||||
case hash
|
||||
when ASTHash
|
||||
@value = @value.merge(hash.value)
|
||||
when Hash
|
||||
@value = @value.merge(hash)
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
"{" + @value.collect { |v| v.collect { |a| a.to_s }.join(' => ') }.join(', ') + "}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -139,6 +139,27 @@ class Puppet::Parser::AST
|
|||
end
|
||||
end
|
||||
|
||||
class HashOrArrayAccess < AST::Leaf
|
||||
attr_accessor :variable, :key
|
||||
|
||||
def evaluate(scope)
|
||||
container = variable.respond_to?(:evaluate) ? variable.safeevaluate(scope) : variable
|
||||
object = (container.is_a?(Hash) or container.is_a?(Array)) ? container : scope.lookupvar(container)
|
||||
|
||||
accesskey = key.respond_to?(:evaluate) ? key.safeevaluate(scope) : key
|
||||
|
||||
unless object.is_a?(Hash) or object.is_a?(Array)
|
||||
raise Puppet::ParseError, "#{variable} is not an hash or array when accessing it with #{accesskey}"
|
||||
end
|
||||
|
||||
return object[accesskey]
|
||||
end
|
||||
|
||||
def to_s
|
||||
"\$#{variable.to_s}[#{key.to_s}]"
|
||||
end
|
||||
end
|
||||
|
||||
class Regex < AST::Leaf
|
||||
def initialize(hash)
|
||||
super
|
||||
|
|
|
@ -131,6 +131,7 @@ namestring: name
|
|||
| funcrvalue
|
||||
| selector
|
||||
| quotedtext
|
||||
| hasharrayaccesses
|
||||
| CLASSNAME {
|
||||
result = ast AST::Name, :value => val[0][:value]
|
||||
}
|
||||
|
@ -325,6 +326,7 @@ resourcename: quotedtext
|
|||
| selector
|
||||
| variable
|
||||
| array
|
||||
| hasharrayaccesses
|
||||
|
||||
assignment: VARIABLE EQUALS expression {
|
||||
if val[0][:value] =~ /::/
|
||||
|
@ -403,6 +405,8 @@ rvalue: quotedtext
|
|||
| selector
|
||||
| variable
|
||||
| array
|
||||
| hash
|
||||
| hasharrayaccesses
|
||||
| resourceref
|
||||
| funcrvalue
|
||||
| undef
|
||||
|
@ -781,6 +785,50 @@ regex: REGEX {
|
|||
result = ast AST::Regex, :value => val[0][:value]
|
||||
}
|
||||
|
||||
hash: LBRACE hashpairs RBRACE {
|
||||
if val[1].instance_of?(AST::ASTHash)
|
||||
result = val[1]
|
||||
else
|
||||
result = ast AST::ASTHash, { :value => val[1] }
|
||||
end
|
||||
}
|
||||
| LBRACE hashpairs COMMA RBRACE {
|
||||
if val[1].instance_of?(AST::ASTHash)
|
||||
result = val[1]
|
||||
else
|
||||
result = ast AST::ASTHash, { :value => val[1] }
|
||||
end
|
||||
} | LBRACE RBRACE {
|
||||
result = ast AST::ASTHash
|
||||
}
|
||||
|
||||
hashpairs: hashpair
|
||||
| hashpairs COMMA hashpair {
|
||||
if val[0].instance_of?(AST::ASTHash)
|
||||
result = val[0].merge(val[2])
|
||||
else
|
||||
result = ast AST::ASTHash, :value => val[0]
|
||||
result.merge(val[2])
|
||||
end
|
||||
}
|
||||
|
||||
hashpair: key FARROW rvalue {
|
||||
result = ast AST::ASTHash, { :value => { val[0] => val[2] } }
|
||||
}
|
||||
|
||||
key: NAME { result = val[0][:value] }
|
||||
| SQTEXT { result = val[0][:value] }
|
||||
| DQTEXT { result = val[0][:value] }
|
||||
|
||||
hasharrayaccess: VARIABLE LBRACK rvalue RBRACK {
|
||||
result = ast AST::HashOrArrayAccess, :variable => val[0][:value], :key => val[2]
|
||||
}
|
||||
|
||||
hasharrayaccesses: hasharrayaccess
|
||||
| hasharrayaccess LBRACK rvalue RBRACK {
|
||||
result = ast AST::HashOrArrayAccess, :variable => val[0], :key => val[2]
|
||||
}
|
||||
|
||||
end
|
||||
---- header ----
|
||||
require 'puppet'
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -336,8 +336,11 @@ class Puppet::Parser::Scope
|
|||
# lookup the value in the scope if it exists and insert the var
|
||||
table[name] = lookupvar(name)
|
||||
# concatenate if string, append if array, nothing for other types
|
||||
if value.is_a?(Array)
|
||||
case value
|
||||
when Array
|
||||
table[name] += value
|
||||
when Hash
|
||||
table[name].merge!(value)
|
||||
else
|
||||
table[name] << value
|
||||
end
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require File.dirname(__FILE__) + '/../../../spec_helper'
|
||||
|
||||
describe Puppet::Parser::AST::ASTHash do
|
||||
before :each do
|
||||
@scope = Puppet::Parser::Scope.new()
|
||||
end
|
||||
|
||||
it "should have a [] accessor" do
|
||||
hash = Puppet::Parser::AST::ASTHash.new(:value => {})
|
||||
hash.should respond_to(:[])
|
||||
end
|
||||
|
||||
it "should have a merge functionality" do
|
||||
hash = Puppet::Parser::AST::ASTHash.new(:value => {})
|
||||
hash.should respond_to(:merge)
|
||||
end
|
||||
|
||||
it "should be able to merge 2 AST hashes" do
|
||||
hash = Puppet::Parser::AST::ASTHash.new(:value => { "a" => "b" })
|
||||
|
||||
hash.merge(Puppet::Parser::AST::ASTHash.new(:value => {"c" => "d"}))
|
||||
|
||||
hash.value.should == { "a" => "b", "c" => "d" }
|
||||
end
|
||||
|
||||
it "should be able to merge with a ruby Hash" do
|
||||
hash = Puppet::Parser::AST::ASTHash.new(:value => { "a" => "b" })
|
||||
|
||||
hash.merge({"c" => "d"})
|
||||
|
||||
hash.value.should == { "a" => "b", "c" => "d" }
|
||||
end
|
||||
|
||||
it "should evaluate each hash value" do
|
||||
key1 = stub "key1"
|
||||
value1 = stub "value1"
|
||||
key2 = stub "key2"
|
||||
value2 = stub "value2"
|
||||
|
||||
value1.expects(:safeevaluate).with(@scope).returns("b")
|
||||
value2.expects(:safeevaluate).with(@scope).returns("d")
|
||||
|
||||
operator = Puppet::Parser::AST::ASTHash.new(:value => { key1 => value1, key2 => value2})
|
||||
operator.evaluate(@scope)
|
||||
end
|
||||
|
||||
it "should return an evaluated hash" do
|
||||
key1 = stub "key1"
|
||||
value1 = stub "value1", :safeevaluate => "b"
|
||||
key2 = stub "key2"
|
||||
value2 = stub "value2", :safeevaluate => "d"
|
||||
|
||||
operator = Puppet::Parser::AST::ASTHash.new(:value => { key1 => value1, key2 => value2})
|
||||
operator.evaluate(@scope).should == { key1 => "b", key2 => "d" }
|
||||
end
|
||||
|
||||
it "should return a valid string with to_s" do
|
||||
hash = Puppet::Parser::AST::ASTHash.new(:value => { "a" => "b", "c" => "d" })
|
||||
|
||||
hash.to_s.should == '{a => b, c => d}'
|
||||
end
|
||||
end
|
|
@ -94,6 +94,89 @@ describe Puppet::Parser::AST::Undef do
|
|||
end
|
||||
end
|
||||
|
||||
describe Puppet::Parser::AST::HashOrArrayAccess do
|
||||
before :each do
|
||||
@scope = stub 'scope'
|
||||
end
|
||||
|
||||
it "should evaluate the variable part if necessary" do
|
||||
@scope.stubs(:lookupvar).with("a").returns(["b"])
|
||||
|
||||
variable = stub 'variable', :evaluate => "a"
|
||||
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => variable, :key => 0 )
|
||||
|
||||
variable.expects(:safeevaluate).with(@scope).returns("a")
|
||||
|
||||
access.evaluate(@scope).should == "b"
|
||||
end
|
||||
|
||||
it "should evaluate the access key part if necessary" do
|
||||
@scope.stubs(:lookupvar).with("a").returns(["b"])
|
||||
|
||||
index = stub 'index', :evaluate => 0
|
||||
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => index )
|
||||
|
||||
index.expects(:safeevaluate).with(@scope).returns(0)
|
||||
|
||||
access.evaluate(@scope).should == "b"
|
||||
end
|
||||
|
||||
it "should be able to return an array member" do
|
||||
@scope.stubs(:lookupvar).with("a").returns(["val1", "val2", "val3"])
|
||||
|
||||
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => 1 )
|
||||
|
||||
access.evaluate(@scope).should == "val2"
|
||||
end
|
||||
|
||||
it "should be able to return an hash value" do
|
||||
@scope.stubs(:lookupvar).with("a").returns({ "key1" => "val1", "key2" => "val2", "key3" => "val3" })
|
||||
|
||||
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" )
|
||||
|
||||
access.evaluate(@scope).should == "val2"
|
||||
end
|
||||
|
||||
it "should raise an error if the variable lookup didn't return an hash or an array" do
|
||||
@scope.stubs(:lookupvar).with("a").returns("I'm a string")
|
||||
|
||||
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" )
|
||||
|
||||
lambda { access.evaluate(@scope) }.should raise_error
|
||||
end
|
||||
|
||||
it "should raise an error if the variable wasn't in the scope" do
|
||||
@scope.stubs(:lookupvar).with("a").returns(nil)
|
||||
|
||||
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" )
|
||||
|
||||
lambda { access.evaluate(@scope) }.should raise_error
|
||||
end
|
||||
|
||||
it "should return a correct string representation" do
|
||||
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" )
|
||||
access.to_s.should == '$a[key2]'
|
||||
end
|
||||
|
||||
it "should work with recursive hash access" do
|
||||
@scope.stubs(:lookupvar).with("a").returns({ "key" => { "subkey" => "b" }})
|
||||
|
||||
access1 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key")
|
||||
access2 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => access1, :key => "subkey")
|
||||
|
||||
access2.evaluate(@scope).should == 'b'
|
||||
end
|
||||
|
||||
it "should work with interleaved array and hash access" do
|
||||
@scope.stubs(:lookupvar).with("a").returns({ "key" => [ "a" , "b" ]})
|
||||
|
||||
access1 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key")
|
||||
access2 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => access1, :key => 1)
|
||||
|
||||
access2.evaluate(@scope).should == 'b'
|
||||
end
|
||||
end
|
||||
|
||||
describe Puppet::Parser::AST::Regex do
|
||||
before :each do
|
||||
@scope = stub 'scope'
|
||||
|
|
|
@ -64,6 +64,11 @@ describe Puppet::Parser::Scope do
|
|||
@scope.lookupvar("var").should == "yep"
|
||||
end
|
||||
|
||||
it "should be able to look up hashes" do
|
||||
@scope.setvar("var", {"a" => "b"})
|
||||
@scope.lookupvar("var").should == {"a" => "b"}
|
||||
end
|
||||
|
||||
it "should be able to look up variables in parent scopes" do
|
||||
@topscope.setvar("var", "parentval")
|
||||
@scope.lookupvar("var").should == "parentval"
|
||||
|
@ -167,6 +172,11 @@ describe Puppet::Parser::Scope do
|
|||
@scope.lookupvar("var").should == [4,2]
|
||||
end
|
||||
|
||||
it "it should store the merged hash {a => b, c => d}" do
|
||||
@topscope.setvar("var",{"a" => "b"}, :append => false)
|
||||
@scope.setvar("var",{"c" => "d"}, :append => true)
|
||||
@scope.lookupvar("var").should == {"a" => "b", "c" => "d"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "when calling number?" do
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
|
||||
$hash = { "file" => "/tmp/myhashfile1" }
|
||||
|
||||
file {
|
||||
$hash["file"]:
|
||||
ensure => file, content => "content";
|
||||
}
|
||||
|
||||
$hash2 = { "a" => { key => "/tmp/myhashfile2" }}
|
||||
|
||||
file {
|
||||
$hash2["a"][key]:
|
||||
ensure => file, content => "content";
|
||||
}
|
||||
|
||||
define test($a = { "b" => "c" }) {
|
||||
file {
|
||||
$a["b"]:
|
||||
ensure => file, content => "content"
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
"test":
|
||||
a => { "b" => "/tmp/myhashfile3" }
|
||||
}
|
||||
|
||||
$hash3 = { mykey => "/tmp/myhashfile4" }
|
||||
$key = "mykey"
|
||||
|
||||
file {
|
||||
$hash3[$key]: ensure => file, content => "content"
|
||||
}
|
|
@ -486,6 +486,13 @@ class TestSnippets < Test::Unit::TestCase
|
|||
assert_file("/tmp/testiftest","if test");
|
||||
end
|
||||
|
||||
def snippet_hash
|
||||
assert_file("/tmp/myhashfile1","hash test 1");
|
||||
assert_file("/tmp/myhashfile2","hash test 2");
|
||||
assert_file("/tmp/myhashfile3","hash test 3");
|
||||
assert_file("/tmp/myhashfile4","hash test 4");
|
||||
end
|
||||
|
||||
# Iterate across each of the snippets and create a test.
|
||||
Dir.entries(snippetdir).sort.each { |file|
|
||||
next if file =~ /^\./
|
||||
|
|
Загрузка…
Ссылка в новой задаче