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:
Brice Figureau 2009-11-10 16:43:37 +01:00 коммит произвёл test branch
Родитель 9122ac5128
Коммит 75c32f910e
11 изменённых файлов: 1475 добавлений и 1004 удалений

Просмотреть файл

@ -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 =~ /^\./