summaryrefslogtreecommitdiffstats
path: root/lib/puppet/parser
diff options
context:
space:
mode:
authorLuke Kanies <luke@madstop.com>2009-12-01 16:41:38 -0800
committerJames Turnbull <james@lovedthanlost.net>2009-12-09 02:13:03 +1100
commit8971d8beae2c409f9052f27c3f80ad3bdfff4de2 (patch)
treec6f7eda0523c31c2b2f3a02b3761bf43ef716ebf /lib/puppet/parser
parent39d4a935d47f1d42241ce492c48818dc5b533c29 (diff)
downloadpuppet-8971d8beae2c409f9052f27c3f80ad3bdfff4de2.tar.gz
puppet-8971d8beae2c409f9052f27c3f80ad3bdfff4de2.tar.xz
puppet-8971d8beae2c409f9052f27c3f80ad3bdfff4de2.zip
Fixing #2596 - Node, Class, Definition are not AST
This commit extracts these three classes into a single ResourceType class in the Parser heirarchy, now completely independent of the AST heirarchy. Most of the other changes are just changing the interface to the new class, which is greatly simplified over the previous classes. This opens up the possibility of drastically simplifying a lot of this other code, too -- in particular, replacing the reference to the parser with a reference to the (soon to be renamed) LoadedCode class. Signed-off-by: Luke Kanies <luke@madstop.com>
Diffstat (limited to 'lib/puppet/parser')
-rw-r--r--lib/puppet/parser/ast.rb3
-rw-r--r--lib/puppet/parser/ast/definition.rb207
-rw-r--r--lib/puppet/parser/ast/hostclass.rb95
-rw-r--r--lib/puppet/parser/ast/leaf.rb24
-rw-r--r--lib/puppet/parser/ast/node.rb42
-rw-r--r--lib/puppet/parser/ast/resource_reference.rb4
-rw-r--r--lib/puppet/parser/compiler.rb8
-rw-r--r--lib/puppet/parser/loaded_code.rb55
-rw-r--r--lib/puppet/parser/parser_support.rb133
-rw-r--r--lib/puppet/parser/resource_type.rb235
10 files changed, 291 insertions, 515 deletions
diff --git a/lib/puppet/parser/ast.rb b/lib/puppet/parser/ast.rb
index ad8af7496..feceb60e0 100644
--- a/lib/puppet/parser/ast.rb
+++ b/lib/puppet/parser/ast.rb
@@ -99,15 +99,12 @@ require 'puppet/parser/ast/casestatement'
require 'puppet/parser/ast/collection'
require 'puppet/parser/ast/collexpr'
require 'puppet/parser/ast/comparison_operator'
-require 'puppet/parser/ast/definition'
require 'puppet/parser/ast/else'
require 'puppet/parser/ast/function'
-require 'puppet/parser/ast/hostclass'
require 'puppet/parser/ast/ifstatement'
require 'puppet/parser/ast/leaf'
require 'puppet/parser/ast/match_operator'
require 'puppet/parser/ast/minus'
-require 'puppet/parser/ast/node'
require 'puppet/parser/ast/nop'
require 'puppet/parser/ast/not'
require 'puppet/parser/ast/resource'
diff --git a/lib/puppet/parser/ast/definition.rb b/lib/puppet/parser/ast/definition.rb
deleted file mode 100644
index 00b0416a0..000000000
--- a/lib/puppet/parser/ast/definition.rb
+++ /dev/null
@@ -1,207 +0,0 @@
-require 'puppet/parser/ast/branch'
-
-require 'puppet/util/warnings'
-
-# The AST class for defined types, which is also the base class
-# nodes and classes.
-class Puppet::Parser::AST::Definition < Puppet::Parser::AST::Branch
- include Puppet::Util::Warnings
- class << self
- attr_accessor :name
- end
-
- associates_doc
-
- # The class name
- @name = :definition
-
- attr_accessor :classname, :arguments, :code, :scope, :keyword
- attr_accessor :exported, :namespace, :parser, :virtual, :name
-
- attr_reader :parentclass
-
- def child_of?(klass)
- false
- end
-
- def get_classname(scope)
- self.classname
- end
-
- # Create a resource that knows how to evaluate our actual code.
- def evaluate(scope)
- resource = Puppet::Parser::Resource.new(:type => self.class.name, :title => get_classname(scope), :scope => scope, :source => scope.source)
-
- scope.catalog.tag(*resource.tags)
-
- scope.compiler.add_resource(scope, resource)
-
- return resource
- end
-
- # Now evaluate the code associated with this class or definition.
- def evaluate_code(resource)
- # Create a new scope.
- scope = subscope(resource.scope, resource)
-
- set_resource_parameters(scope, resource)
-
- if self.code
- return self.code.safeevaluate(scope)
- else
- return nil
- end
- end
-
- def initialize(hash = {})
- @arguments = nil
- @parentclass = nil
- super
-
- # Convert the arguments to a hash for ease of later use.
- if @arguments
- unless @arguments.is_a? Array
- @arguments = [@arguments]
- end
- oldargs = @arguments
- @arguments = {}
- oldargs.each do |arg, val|
- @arguments[arg] = val
- end
- else
- @arguments = {}
- end
-
- # Deal with metaparams in the argument list.
- @arguments.each do |arg, defvalue|
- next unless Puppet::Type.metaparamclass(arg)
- if defvalue
- warnonce "%s is a metaparam; this value will inherit to all contained resources" % arg
- else
- raise Puppet::ParseError, "%s is a metaparameter; please choose another parameter name in the %s definition" % [arg, self.classname]
- end
- end
- end
-
- def find_parentclass
- @parser.find_hostclass(namespace, parentclass)
- end
-
- # Set our parent class, with a little check to avoid some potential
- # weirdness.
- def parentclass=(name)
- if name == self.classname
- parsefail "Parent classes must have dissimilar names"
- end
-
- @parentclass = name
- end
-
- # Hunt down our class object.
- def parentobj
- return nil unless @parentclass
-
- # Cache our result, since it should never change.
- unless defined?(@parentobj)
- unless tmp = find_parentclass
- parsefail "Could not find %s parent %s" % [self.class.name, @parentclass]
- end
-
- if tmp == self
- parsefail "Parent classes must have dissimilar names"
- end
-
- @parentobj = tmp
- end
- @parentobj
- end
-
- # Create a new subscope in which to evaluate our code.
- def subscope(scope, resource)
- args = {
- :resource => resource,
- :keyword => self.keyword,
- :namespace => self.namespace,
- :source => self
- }
-
- oldscope = scope
- scope = scope.newscope(args)
- scope.source = self
-
- return scope
- end
-
- def to_s
- classname
- end
-
- # Check whether a given argument is valid. Searches up through
- # any parent classes that might exist.
- def validattr?(param)
- param = param.to_s
-
- if @arguments.include?(param)
- # It's a valid arg for us
- return true
- elsif param == "name"
- return true
-# elsif defined? @parentclass and @parentclass
-# # Else, check any existing parent
-# if parent = @scope.lookuptype(@parentclass) and parent != []
-# return parent.validarg?(param)
-# elsif builtin = Puppet::Type.type(@parentclass)
-# return builtin.validattr?(param)
-# else
-# raise Puppet::Error, "Could not find parent class %s" %
-# @parentclass
-# end
- elsif Puppet::Type.metaparam?(param)
- return true
- else
- # Or just return false
- return false
- end
- end
-
- private
-
- # Set any arguments passed by the resource as variables in the scope.
- def set_resource_parameters(scope, resource)
- args = symbolize_options(resource.to_hash || {})
-
- # Verify that all required arguments are either present or
- # have been provided with defaults.
- if self.arguments
- self.arguments.each { |arg, default|
- arg = arg.to_sym
- unless args.include?(arg)
- if defined? default and ! default.nil?
- default = default.safeevaluate scope
- args[arg] = default
- #Puppet.debug "Got default %s for %s in %s" %
- # [default.inspect, arg.inspect, @name.inspect]
- else
- parsefail "Must pass %s to %s of type %s" %
- [arg, resource.title, @classname]
- end
- end
- }
- end
-
- # Set each of the provided arguments as variables in the
- # definition's scope.
- args.each { |arg,value|
- unless validattr?(arg)
- parsefail "%s does not accept attribute %s" % [@classname, arg]
- end
-
- exceptwrap do
- scope.setvar(arg.to_s, args[arg])
- end
- }
-
- scope.setvar("title", resource.title) unless args.include? :title
- scope.setvar("name", resource.name) unless args.include? :name
- end
-end
diff --git a/lib/puppet/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb
deleted file mode 100644
index 23d9a00e5..000000000
--- a/lib/puppet/parser/ast/hostclass.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-require 'puppet/parser/ast/definition'
-
-# The code associated with a class. This is different from definitions
-# in that each class is a singleton -- only one will exist for a given
-# node.
-class Puppet::Parser::AST::HostClass < Puppet::Parser::AST::Definition
-
- associates_doc
-
- @name = :class
-
- # Are we a child of the passed class? Do a recursive search up our
- # parentage tree to figure it out.
- def child_of?(klass)
- return false unless self.parentclass
-
- if klass == self.parentobj
- return true
- else
- return self.parentobj.child_of?(klass)
- end
- end
-
- # Make sure our parent class has been evaluated, if we have one.
- def evaluate(scope)
- if parentclass and ! scope.catalog.resource(self.class.name, parentclass)
- parent_resource = parentobj.evaluate(scope)
- end
-
- # Do nothing if the resource already exists; this makes sure we don't
- # get multiple copies of the class resource, which helps provide the
- # singleton nature of classes.
- if resource = scope.catalog.resource(self.class.name, self.classname)
- return resource
- end
-
- super
- end
-
- # Evaluate the code associated with this class.
- def evaluate_code(resource)
- scope = resource.scope
- # Verify that we haven't already been evaluated. This is
- # what provides the singleton aspect.
- if existing_scope = scope.compiler.class_scope(self)
- Puppet.debug "Class '%s' already evaluated; not evaluating again" % (classname == "" ? "main" : classname)
- return nil
- end
-
- pnames = nil
- if pklass = self.parentobj
- parent_resource = resource.scope.compiler.catalog.resource(self.class.name, pklass.classname)
- # This shouldn't evaluate if the class has already been evaluated.
- pklass.evaluate_code(parent_resource)
-
- scope = parent_scope(scope, pklass)
- pnames = scope.namespaces
- end
-
- # Don't create a subscope for the top-level class, since it already
- # has its own scope.
- unless resource.title == :main
- scope = subscope(scope, resource)
-
- scope.setvar("title", resource.title)
- scope.setvar("name", resource.name)
- end
-
- # Add the parent scope namespaces to our own.
- if pnames
- pnames.each do |ns|
- scope.add_namespace(ns)
- end
- end
-
- # Set the class before we evaluate the code, so that it's set during
- # the evaluation and can be inspected.
- scope.compiler.class_set(self.classname, scope)
-
- # Now evaluate our code, yo.
- if self.code
- return self.code.safeevaluate(scope)
- else
- return nil
- end
- end
-
- def parent_scope(scope, klass)
- if s = scope.compiler.class_scope(klass)
- return s
- else
- raise Puppet::DevError, "Could not find scope for %s" % klass.classname
- end
- end
-end
diff --git a/lib/puppet/parser/ast/leaf.rb b/lib/puppet/parser/ast/leaf.rb
index b73c781e1..07bba1b4c 100644
--- a/lib/puppet/parser/ast/leaf.rb
+++ b/lib/puppet/parser/ast/leaf.rb
@@ -94,6 +94,7 @@ class Puppet::Parser::AST
def initialize(hash)
super
+ # Note that this is an AST::Regex, not a Regexp
@value = @value.to_s.downcase unless @value.is_a?(Regex)
if @value =~ /[^-\w.]/
raise Puppet::DevError,
@@ -101,10 +102,6 @@ class Puppet::Parser::AST
end
end
- def to_classname
- to_s.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'')
- end
-
# implementing eql? and hash so that when an HostName is stored
# in a hash it has the same hashing properties as the underlying value
def eql?(value)
@@ -116,25 +113,6 @@ class Puppet::Parser::AST
return @value.hash
end
- def match(value)
- return @value.match(value) unless value.is_a?(HostName)
-
- if value.regex? and self.regex?
- # Wow this is some sweet design; maybe a touch of refactoring
- # in order here.
- return value.value.value == self.value.value
- elsif value.regex? # we know if the existing name is not a regex, it won't match a regex
- return false
- else
- # else, we could be either a regex or normal and it doesn't matter
- return @value.match(value.value)
- end
- end
-
- def regex?
- @value.is_a?(Regex)
- end
-
def to_s
@value.to_s
end
diff --git a/lib/puppet/parser/ast/node.rb b/lib/puppet/parser/ast/node.rb
deleted file mode 100644
index 4f75201eb..000000000
--- a/lib/puppet/parser/ast/node.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require 'puppet/parser/ast/hostclass'
-
-# The specific code associated with a host. Nodes are annoyingly unlike
-# other objects. That's just the way it is, at least for now.
-class Puppet::Parser::AST::Node < Puppet::Parser::AST::HostClass
-
- associates_doc
-
- @name = :node
-
- def initialize(options)
- @parentclass = nil
- super
- end
-
- def namespace
- ""
- end
-
- # in Regex mode, our classname can't be our Regex.
- # so we use the currently connected client as our
- # classname, mimicing exactly what would have happened
- # if there was a specific node definition for this node.
- def get_classname(scope)
- return scope.host if name.regex?
- classname
- end
-
- # Make sure node scopes are marked as such.
- def subscope(*args)
- scope = super
- scope.nodescope = true
- scope
- end
-
- private
-
- # Search for the object matching our parent class.
- def find_parentclass
- @parser.node(parentclass)
- end
-end
diff --git a/lib/puppet/parser/ast/resource_reference.rb b/lib/puppet/parser/ast/resource_reference.rb
index 117bc88af..794e505b8 100644
--- a/lib/puppet/parser/ast/resource_reference.rb
+++ b/lib/puppet/parser/ast/resource_reference.rb
@@ -44,7 +44,7 @@ class Puppet::Parser::AST
def qualified_class(scope, title)
# Look up the full path to the class
if classobj = scope.find_hostclass(title)
- title = classobj.classname
+ title = classobj.name
else
raise Puppet::ParseError, "Could not find class %s" % title
end
@@ -57,7 +57,7 @@ class Puppet::Parser::AST
objtype = @type.downcase
unless builtintype?(objtype)
if dtype = scope.find_definition(objtype)
- objtype = dtype.classname
+ objtype = dtype.name
else
raise Puppet::ParseError, "Could not find resource type %s" % objtype
end
diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb
index 77b0bb98e..6b6cd6815 100644
--- a/lib/puppet/parser/compiler.rb
+++ b/lib/puppet/parser/compiler.rb
@@ -71,8 +71,8 @@ class Puppet::Parser::Compiler
# variables.
def class_scope(klass)
# They might pass in either the class or class name
- if klass.respond_to?(:classname)
- @class_scopes[klass.classname]
+ if klass.respond_to?(:name)
+ @class_scopes[klass.name]
else
@class_scopes[klass]
end
@@ -140,7 +140,7 @@ class Puppet::Parser::Compiler
if klass = scope.find_hostclass(name)
found << name and next if class_scope(klass)
- resource = klass.evaluate(scope)
+ resource = klass.mk_plain_resource(scope)
# If they've disabled lazy evaluation (which the :include function does),
# then evaluate our resource immediately.
@@ -230,7 +230,7 @@ class Puppet::Parser::Compiler
# Create a resource to model this node, and then add it to the list
# of resources.
- resource = astnode.evaluate(topscope)
+ resource = astnode.mk_plain_resource(topscope)
resource.evaluate
diff --git a/lib/puppet/parser/loaded_code.rb b/lib/puppet/parser/loaded_code.rb
index 3efd115bc..d7f179ae7 100644
--- a/lib/puppet/parser/loaded_code.rb
+++ b/lib/puppet/parser/loaded_code.rb
@@ -8,44 +8,62 @@ class Puppet::Parser::LoadedCode
@node_list = []
end
- def add_hostclass(name, code)
- @hostclasses[munge_name(name)] = code
+ def <<(thing)
+ add(thing)
+ self
+ end
+
+ def add(instance)
+ method = "add_#{instance.type}"
+ send(method, instance)
+ instance.code_collection = self
+ instance
+ end
+
+ def add_hostclass(instance)
+ dupe_check(instance, @hostclasses) { |dupe| "Class #{instance.name} is already defined#{dupe.error_context}; cannot redefine" }
+ dupe_check(instance, @definitions) { |dupe| "Definition #{instance.name} is already defined#{dupe.error_context}; cannot be redefined as a class" }
+
+ @hostclasses[instance.name] = instance
+ instance
end
def hostclass(name)
@hostclasses[munge_name(name)]
end
- def add_node(name, code)
- name = check_name(name)
- @node_list << name unless @node_list.include?(name)
- @nodes[name] = code
+ def add_node(instance)
+ dupe_check(instance, @nodes) { |dupe| "Node #{instance.name} is already defined#{dupe.error_context}; cannot redefine" }
+
+ @node_list << instance
+ @nodes[instance.name] = instance
+ instance
end
def node(name)
- name = check_name(name)
+ name = munge_name(name)
if node = @nodes[name]
return node
end
- @node_list.each do |nodename|
- n = @nodes[nodename]
- return n if nodename.regex? and nodename.match(name)
+ @node_list.each do |node|
+ next unless node.name_is_regex?
+ return node if node.match(name)
end
nil
end
def node_exists?(name)
- @nodes[check_name(name)]
+ @nodes[munge_name(name)]
end
def nodes?
@nodes.length > 0
end
- def add_definition(name, code)
- @definitions[munge_name(name)] = code
+ def add_definition(code)
+ @definitions[code.name] = code
end
def definition(name)
@@ -108,12 +126,9 @@ class Puppet::Parser::LoadedCode
name.to_s.downcase
end
- # Check that the given (node) name is an HostName instance
- # We're doing this so that hashing of node in the @nodes hash
- # is consistent (see AST::HostName#hash and AST::HostName#eql?)
- # and that the @nodes hash still keep its O(1) get/put properties.
- def check_name(name)
- name = Puppet::Parser::AST::HostName.new(:value => name) unless name.is_a?(Puppet::Parser::AST::HostName)
- name
+ def dupe_check(instance, hash)
+ return unless dupe = hash[instance.name]
+ message = yield dupe
+ instance.fail Puppet::ParseError, message
end
end
diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb
index 7a8fa812a..1b961cd04 100644
--- a/lib/puppet/parser/parser_support.rb
+++ b/lib/puppet/parser/parser_support.rb
@@ -4,6 +4,7 @@ class Puppet::Parser::Parser
require 'puppet/parser/functions'
require 'puppet/parser/files'
require 'puppet/parser/loaded_code'
+ require 'puppet/parser/resource_type'
require 'monitor'
AST = Puppet::Parser::AST
@@ -42,17 +43,16 @@ class Puppet::Parser::Parser
# Create an AST object, and automatically add the file and line information if
# available.
def ast(klass, hash = {})
- hash[:line] = @lexer.line unless hash.include?(:line)
-
- unless hash.include?(:file)
- if file = @lexer.file
- hash[:file] = file
- end
- end
+ klass.new ast_context(klass.use_docs).merge(hash)
+ end
- k = klass.new(hash)
- k.doc = lexer.getcomment(hash[:line]) if !k.nil? and k.use_docs and k.doc.empty?
- return k
+ def ast_context(include_docs = false)
+ result = {
+ :line => lexer.line,
+ :file => lexer.file
+ }
+ result[:doc] = lexer.getcomment(result[:line]) if include_docs
+ result
end
# The fully qualifed name, with the full namespace.
@@ -276,126 +276,21 @@ class Puppet::Parser::Parser
# Create a new class, or merge with an existing class.
def newclass(name, options = {})
- name = name.downcase
-
- if @loaded_code.definition(name)
- raise Puppet::ParseError, "Cannot redefine class %s as a definition" % name
- end
- code = options[:code]
- parent = options[:parent]
- doc = options[:doc]
-
- # If the class is already defined, then add code to it.
- if other = @loaded_code.hostclass(name) || @loaded_code.definition(name)
- # Make sure the parents match
- if parent and other.parentclass and (parent != other.parentclass)
- error("Class %s is already defined at %s:%s; cannot redefine" % [name, other.file, other.line])
- end
-
- # This might be dangerous...
- if parent and ! other.parentclass
- other.parentclass = parent
- end
-
- # This might just be an empty, stub class.
- if code
- tmp = name
- if tmp == ""
- tmp = "main"
- end
-
- Puppet.debug addcontext("Adding code to %s" % tmp)
- # Else, add our code to it.
- if other.code and code
- # promote if neededcodes to ASTArray so that we can append code
- # ASTArray knows how to evaluate its members.
- other.code = ast AST::ASTArray, :children => [other.code] unless other.code.is_a?(AST::ASTArray)
- code = ast AST::ASTArray, :children => [code] unless code.is_a?(AST::ASTArray)
- other.code.children += code.children
- else
- other.code ||= code
- end
- end
-
- if other.doc and doc
- other.doc += doc
- else
- other.doc ||= doc
- end
- else
- # Define it anew.
- # Note we're doing something somewhat weird here -- we're setting
- # the class's namespace to its fully qualified name. This means
- # anything inside that class starts looking in that namespace first.
- args = {:namespace => name, :classname => name, :parser => self}
- args[:code] = code if code
- args[:parentclass] = parent if parent
- args[:doc] = doc
- args[:line] = options[:line]
-
- @loaded_code.add_hostclass(name, ast(AST::HostClass, args))
- end
-
- return @loaded_code.hostclass(name)
+ @loaded_code.add Puppet::Parser::ResourceType.new(:hostclass, name, ast_context(true).merge(options))
end
# Create a new definition.
def newdefine(name, options = {})
- name = name.downcase
- if @loaded_code.hostclass(name)
- raise Puppet::ParseError, "Cannot redefine class %s as a definition" %
- name
- end
- # Make sure our definition doesn't already exist
- if other = @loaded_code.definition(name)
- error("%s is already defined at %s:%s; cannot redefine" % [name, other.file, other.line])
- end
-
- ns, whatever = namesplit(name)
- args = {
- :namespace => ns,
- :arguments => options[:arguments],
- :code => options[:code],
- :parser => self,
- :classname => name,
- :doc => options[:doc],
- :line => options[:line]
- }
-
- [:code, :arguments].each do |param|
- args[param] = options[param] if options[param]
- end
-
- @loaded_code.add_definition(name, ast(AST::Definition, args))
+ @loaded_code.add Puppet::Parser::ResourceType.new(:definition, name, ast_context(true).merge(options))
end
# Create a new node. Nodes are special, because they're stored in a global
# table, not according to namespaces.
def newnode(names, options = {})
names = [names] unless names.instance_of?(Array)
- doc = lexer.getcomment
+ context = ast_context(true)
names.collect do |name|
- name = AST::HostName.new :value => name unless name.is_a?(AST::HostName)
- if other = @loaded_code.node_exists?(name)
- error("Node %s is already defined at %s:%s; cannot redefine" % [other.name, other.file, other.line])
- end
- name = name.to_s if name.is_a?(Symbol)
- args = {
- :name => name,
- :parser => self,
- :doc => doc,
- :line => options[:line]
- }
- if options[:code]
- args[:code] = options[:code]
- end
- if options[:parent]
- args[:parentclass] = options[:parent]
- end
- node = ast(AST::Node, args)
- node.classname = name.to_classname
- @loaded_code.add_node(name, node)
- node
+ @loaded_code.add(Puppet::Parser::ResourceType.new(:node, name, context.merge(options)))
end
end
diff --git a/lib/puppet/parser/resource_type.rb b/lib/puppet/parser/resource_type.rb
new file mode 100644
index 000000000..a67965960
--- /dev/null
+++ b/lib/puppet/parser/resource_type.rb
@@ -0,0 +1,235 @@
+require 'puppet/parser/parser'
+require 'puppet/util/warnings'
+require 'puppet/util/errors'
+require 'puppet/util/inline_docs'
+require 'puppet/parser/ast/leaf'
+
+class Puppet::Parser::ResourceType
+ include Puppet::Util::InlineDocs
+ include Puppet::Util::Warnings
+ include Puppet::Util::Errors
+
+ RESOURCE_SUPERTYPES = [:hostclass, :node, :definition]
+
+ attr_accessor :file, :line, :doc, :code, :parent, :code_collection
+ attr_reader :type, :namespace, :arguments, :behaves_like
+
+ # Are we a child of the passed class? Do a recursive search up our
+ # parentage tree to figure it out.
+ def child_of?(klass)
+ return false unless parent
+
+ return true if klass == parent_type
+ return parent_type.child_of?(klass)
+ end
+
+ # Now evaluate the code associated with this class or definition.
+ def evaluate_code(resource)
+ # Create a new scope.
+ scope = subscope(resource.scope, resource)
+
+ set_resource_parameters(resource, scope)
+
+ return nil unless c = self.code
+ return c.safeevaluate(scope)
+ end
+
+ def initialize(type, name, options = {})
+ @type = type.to_s.downcase.to_sym
+ raise ArgumentError, "Invalid resource supertype '#{type}'" unless RESOURCE_SUPERTYPES.include?(@type)
+
+ name = convert_from_ast(name) if name.is_a?(Puppet::Parser::AST::HostName)
+
+ set_name_and_namespace(name)
+
+ [:code, :doc, :line, :file, :parent].each do |param|
+ next unless value = options[param]
+ send(param.to_s + "=", value)
+ end
+
+ set_arguments(options[:arguments])
+ end
+
+ # This is only used for node names, and really only when the node name
+ # is a regexp.
+ def match(string)
+ return string.to_s.downcase == name unless name_is_regex?
+
+ return @name =~ string
+ end
+
+ # Add code from a new instance to our code.
+ def merge(other)
+ fail ArgumentError, "#{name} is not a class; cannot add code to it" unless type == :hostclass
+ fail ArgumentError, "#{other.name} is not a class; cannot add code from it" unless other.type == :hostclass
+
+ if parent and other.parent and parent != other.parent
+ fail ArgumentError, "Cannot merge classes with different parent classes"
+ end
+
+ # We know they're either equal or only one is set, so keep whichever parent is specified.
+ self.parent ||= other.parent
+
+ if other.doc
+ self.doc ||= ""
+ self.doc += other.doc
+ end
+
+ # This might just be an empty, stub class.
+ return unless other.code
+
+ unless self.code
+ self.code = other.code
+ return
+ end
+
+ array_class = Puppet::Parser::AST::ASTArray
+ unless self.code.is_a?(array_class)
+ self.code = array_class.new(:children => [self.code])
+ end
+
+ if other.code.is_a?(array_class)
+ code.children += other.code.children
+ else
+ code.children << other.code
+ end
+ end
+
+ # Make an instance of our resource type. This is only possible
+ # for those classes and nodes that don't have any arguments, and is
+ # only useful for things like the 'include' function.
+ def mk_plain_resource(scope)
+ type == :definition and raise ArgumentError, "Cannot create resources for defined resource types"
+ resource_type = type == :hostclass ? :class : :node
+
+ # Make sure our parent class has been evaluated, if we have one.
+ if parent and ! scope.catalog.resource(resource_type, parent)
+ parent_type.mk_plain_resource(scope)
+ end
+
+ # Do nothing if the resource already exists; this makes sure we don't
+ # get multiple copies of the class resource, which helps provide the
+ # singleton nature of classes.
+ if resource = scope.catalog.resource(resource_type, name)
+ return resource
+ end
+
+ resource = Puppet::Parser::Resource.new(:type => resource_type, :title => name, :scope => scope, :source => self)
+ scope.compiler.add_resource(scope, resource)
+ scope.catalog.tag(*resource.tags)
+ resource
+ end
+
+ def name
+ return @name unless @name.is_a?(Regexp)
+ return @name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'')
+ end
+
+ def name_is_regex?
+ @name.is_a?(Regexp)
+ end
+
+ def parent_type
+ return nil unless parent
+
+ unless @parent_type ||= code_collection.send(type, parent)
+ fail Puppet::ParseError, "Could not find parent resource type '#{parent}'"
+ end
+
+ @parent_type
+ end
+
+ # Set any arguments passed by the resource as variables in the scope.
+ def set_resource_parameters(resource, scope)
+ set = {}
+ resource.to_hash.each do |param, value|
+ param = param.to_sym
+ fail Puppet::ParseError, "#{resource.ref} does not accept attribute #{param}" unless validattr?(param)
+
+ exceptwrap { scope.setvar(param.to_s, value) }
+
+ set[param] = true
+ end
+
+ # Verify that all required arguments are either present or
+ # have been provided with defaults.
+ arguments.each do |param, default|
+ param = param.to_sym
+ next if set.include?(param)
+
+ # Even if 'default' is a false value, it's an AST value, so this works fine
+ fail Puppet::ParseError, "Must pass #{param} to #{resource.ref}" unless default
+
+ scope.setvar(param.to_s, default.safeevaluate(scope))
+ end
+
+ scope.setvar("title", resource.title) unless set.include? :title
+ scope.setvar("name", resource.name) unless set.include? :name
+ end
+
+ # Create a new subscope in which to evaluate our code.
+ def subscope(scope, resource)
+ scope.newscope :resource => resource, :namespace => self.namespace, :source => self
+ end
+
+ # Check whether a given argument is valid.
+ def validattr?(param)
+ param = param.to_s
+
+ return true if param == "name"
+ return true if Puppet::Type.metaparam?(param)
+ return false unless defined?(@arguments)
+ return true if arguments.include?(param)
+ return false
+ end
+
+ def set_arguments(arguments)
+ @arguments = {}
+ return if arguments.nil?
+
+ arguments.each do |arg, default|
+ arg = arg.to_s
+ warn_if_metaparam(arg, default)
+ @arguments[arg] = default
+ end
+ end
+
+ private
+
+ def convert_from_ast(name)
+ value = name.value
+ if value.is_a?(Puppet::Parser::AST::Regex)
+ name = value.value
+ else
+ name = value
+ end
+ end
+
+ # Split an fq name into a namespace and name
+ def namesplit(fullname)
+ ary = fullname.split("::")
+ n = ary.pop || ""
+ ns = ary.join("::")
+ return ns, n
+ end
+
+ def set_name_and_namespace(name)
+ if name.is_a?(Regexp)
+ @name = name
+ @namespace = ""
+ else
+ @name = name.to_s.downcase
+ @namespace, ignored_shortname = namesplit(@name)
+ end
+ end
+
+ def warn_if_metaparam(param, default)
+ return unless Puppet::Type.metaparamclass(param)
+
+ if default
+ warnonce "#{param} is a metaparam; this value will inherit to all contained resources"
+ else
+ raise Puppet::ParseError, "#{param} is a metaparameter; please choose another parameter name in the #{self.name} definition"
+ end
+ end
+end