Hopefully last patch in the series:

* Added syntactical tests
* Added lexer tests
* Slightly refactored some internal code

[real commit message]

You can now specify relationships directly in the language:

  File[/foo] -> Service[bar]

Specifies a normal dependency while:

  File[/foo] ~> Service[bar]

Specifies a subscription.

You can also do relationship chaining, specifying multiple
relationships on a single line:

  File[/foo] -> Package[baz] -> Service[bar]

Note that while it's confusing, you don't have to have all
of the arrows be the same direction:

  File[/foo] -> Service[bar] <~ Package[baz]

This can provide some succinctness at the cost of readability.

You can also specify full resources, rather than just
resource refs:

    file { "/foo": ensure => present } -> package { bar: ensure => installed }

But wait! There's more!  You can also specify a subscription on either side
of the relationship marker:

    yumrepo { foo: .... }
    package { bar: provider => yum, ... }
    Yumrepo <| |> -> Package <| provider == yum |>

This, finally, provides easy many to many relationships in Puppet, but it also 
opens
the door to massive dependency cycles.  This last feature is a very powerful 
stick,
and you can considerably hurt yourself with it.

Signed-off-by: Luke Kanies <[email protected]>
---
 lib/puppet/parser/ast.rb              |    1 +
 lib/puppet/parser/ast/relationship.rb |   60 +
 lib/puppet/parser/compiler.rb         |   15 +-
 lib/puppet/parser/grammar.ra          |   14 +-
 lib/puppet/parser/lexer.rb            |    4 +
 lib/puppet/parser/parser.rb           | 2309 +++++++++++++++++----------------
 lib/puppet/parser/relationship.rb     |   43 +
 spec/integration/parser/parser.rb     |   92 ++
 spec/unit/parser/ast/relationship.rb  |   88 ++
 spec/unit/parser/compiler.rb          |   11 +-
 spec/unit/parser/lexer.rb             |    4 +
 spec/unit/parser/relationship.rb      |   70 +
 12 files changed, 1587 insertions(+), 1124 deletions(-)
 create mode 100644 lib/puppet/parser/ast/relationship.rb
 create mode 100644 lib/puppet/parser/relationship.rb
 create mode 100644 spec/unit/parser/ast/relationship.rb
 create mode 100644 spec/unit/parser/relationship.rb

diff --git a/lib/puppet/parser/ast.rb b/lib/puppet/parser/ast.rb
index 6af7745..06adb8b 100644
--- a/lib/puppet/parser/ast.rb
+++ b/lib/puppet/parser/ast.rb
@@ -116,3 +116,4 @@ require 'puppet/parser/ast/resourceparam'
 require 'puppet/parser/ast/selector'
 require 'puppet/parser/ast/tag'
 require 'puppet/parser/ast/vardef'
+require 'puppet/parser/ast/relationship'
diff --git a/lib/puppet/parser/ast/relationship.rb 
b/lib/puppet/parser/ast/relationship.rb
new file mode 100644
index 0000000..9f9f6fc
--- /dev/null
+++ b/lib/puppet/parser/ast/relationship.rb
@@ -0,0 +1,60 @@
+require 'puppet/parser/ast'
+require 'puppet/parser/ast/branch'
+require 'puppet/parser/relationship'
+
+class Puppet::Parser::AST::Relationship < Puppet::Parser::AST::Branch
+    RELATIONSHIP_TYPES = %w{-> <- ~> <~}
+
+    attr_accessor :left, :right, :arrow, :type
+
+    def actual_left
+        chained? ? left.right : left
+    end
+
+    # Evaluate our object, but just return a simple array of the type
+    # and name.
+    def evaluate(scope)
+        if chained?
+            real_left = left.safeevaluate(scope)
+            left_dep = left_dep.shift if left_dep.is_a?(Array)
+        else
+            real_left = left.safeevaluate(scope)
+        end
+        real_right = right.safeevaluate(scope)
+
+        source, target = sides2edge(real_left, real_right)
+        result = Puppet::Parser::Relationship.new(source, target, type)
+        scope.compiler.add_relationship(result)
+        real_right
+    end
+
+    def initialize(left, right, arrow, args = {})
+        super(args)
+        unless RELATIONSHIP_TYPES.include?(arrow)
+            raise ArgumentError, "Invalid relationship type #{arrow.inspect}; 
valid types are #{RELATIONSHIP_TYPES.collect { |r| r.to_s }.join(", ")}"
+        end
+        @left, @right, @arrow = left, right, arrow
+    end
+
+    def type
+        subscription? ? :subscription : :relationship
+    end
+
+    def sides2edge(left, right)
+        out_edge? ? [left, right] : [right, left]
+    end
+
+    private
+
+    def chained?
+        left.is_a?(self.class)
+    end
+
+    def out_edge?
+        ["->", "~>"].include?(arrow)
+    end
+
+    def subscription?
+        ["~>", "<~"].include?(arrow)
+    end
+end
diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb
index 8e84f5a..fd7d968 100644
--- a/lib/puppet/parser/compiler.rb
+++ b/lib/puppet/parser/compiler.rb
@@ -21,13 +21,17 @@ class Puppet::Parser::Compiler
         raise Puppet::Error, "#{detail} on node #{node.name}"
     end
 
-    attr_reader :node, :facts, :collections, :catalog, :node_scope, :resources
+    attr_reader :node, :facts, :collections, :catalog, :node_scope, 
:resources, :relationships
 
     # Add a collection to the global list.
     def add_collection(coll)
         @collections << coll
     end
 
+    def add_relationship(dep)
+        @relationships << dep
+    end
+
     # Store a resource override.
     def add_override(override)
         # If possible, merge the override in immediately.
@@ -144,6 +148,10 @@ class Puppet::Parser::Compiler
         found
     end
 
+    def evaluate_relationships
+        @relationships.each { |rel| rel.evaluate(catalog) }
+    end
+
     # Return a resource by either its ref or its type and title.
     def findresource(*args)
         @catalog.resource(*args)
@@ -337,6 +345,8 @@ class Puppet::Parser::Compiler
     # Make sure all of our resources and such have done any last work
     # necessary.
     def finish
+        evaluate_relationships()
+
         resources.each do |resource|
             # Add in any resource overrides.
             if overrides = resource_overrides(resource)
@@ -374,6 +384,9 @@ class Puppet::Parser::Compiler
         # but they each refer back to the scope that created them.
         @collections = []
 
+        # The list of relationships to evaluate.
+        @relationships = []
+
         # For maintaining the relationship between scopes and their resources.
         @catalog = Puppet::Resource::Catalog.new(@node.name)
         @catalog.version = known_resource_types.version
diff --git a/lib/puppet/parser/grammar.ra b/lib/puppet/parser/grammar.ra
index 5b9a609..31cc9a4 100644
--- a/lib/puppet/parser/grammar.ra
+++ b/lib/puppet/parser/grammar.ra
@@ -11,7 +11,7 @@ token QMARK LPAREN RPAREN ISEQUAL GREATEREQUAL GREATERTHAN 
LESSTHAN
 token IF ELSE IMPORT DEFINE ELSIF VARIABLE CLASS INHERITS NODE BOOLEAN
 token NAME SEMIC CASE DEFAULT AT LCOLLECT RCOLLECT CLASSNAME CLASSREF
 token NOT OR AND UNDEF PARROW PLUS MINUS TIMES DIV LSHIFT RSHIFT UMINUS
-token MATCH NOMATCH REGEX
+token MATCH NOMATCH REGEX IN_EDGE OUT_EDGE IN_EDGE_SUB OUT_EDGE_SUB
 
 prechigh
     right NOT
@@ -74,6 +74,18 @@ statement:    resource
             | nodedef
             | resourceoverride
             | append
+            | relationship
+
+relationship: relationship_side edge relationship_side {
+    result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context)
+}
+            | relationship edge relationship_side {
+    result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context)
+}
+
+relationship_side: resource | resourceref | collection
+
+edge: IN_EDGE | OUT_EDGE | IN_EDGE_SUB | OUT_EDGE_SUB
 
 fstatement:   NAME LPAREN funcvalues RPAREN {
     args = aryfy(val[2])
diff --git a/lib/puppet/parser/lexer.rb b/lib/puppet/parser/lexer.rb
index 2a1f88e..9a9b852 100644
--- a/lib/puppet/parser/lexer.rb
+++ b/lib/puppet/parser/lexer.rb
@@ -129,6 +129,10 @@ class Puppet::Parser::Lexer
         ':' => :COLON,
         '@' => :AT,
         '<<|' => :LLCOLLECT,
+        '->' => :IN_EDGE,
+        '<-' => :OUT_EDGE,
+        '~>' => :IN_EDGE_SUB,
+        '<~' => :OUT_EDGE_SUB,
         '|>>' => :RRCOLLECT,
         '<|' => :LCOLLECT,
         '|>' => :RCOLLECT,
diff --git a/lib/puppet/parser/parser.rb b/lib/puppet/parser/parser.rb
index 5fecc54..ac4205a 100644
--- a/lib/puppet/parser/parser.rb
+++ b/lib/puppet/parser/parser.rb
[removed generated code]
diff --git a/lib/puppet/parser/relationship.rb 
b/lib/puppet/parser/relationship.rb
new file mode 100644
index 0000000..1d1bad7
--- /dev/null
+++ b/lib/puppet/parser/relationship.rb
@@ -0,0 +1,43 @@
+class Puppet::Parser::Relationship
+    attr_accessor :source, :target, :type
+
+    PARAM_MAP = {:relationship => :before, :subscription => :notify}
+
+    def evaluate(catalog)
+        if source.is_a?(Puppet::Parser::Collector)
+            sources = source.collected.values
+        else
+            sources = [source]
+        end
+        if target.is_a?(Puppet::Parser::Collector)
+            targets = target.collected.values
+        else
+            targets = [target]
+        end
+        sources.each do |s|
+            targets.each do |t|
+                mk_relationship(s, t, catalog)
+            end
+        end
+    end
+
+    def initialize(source, target, type)
+        @source, @target, @type = source, target, type
+    end
+
+    def param_name
+        PARAM_MAP[type] || raise(ArgumentError, "Invalid relationship type 
#{type}")
+    end
+
+    def mk_relationship(source, target, catalog)
+        unless source_resource = catalog.resource(source.to_s)
+            raise ArgumentError, "Could not find resource '#{source}' for 
relationship on '#{target}'"
+        end
+        unless target_resource = catalog.resource(target.to_s)
+            raise ArgumentError, "Could not find resource '#{target}' for 
relationship from '#{source}'"
+        end
+        Puppet.debug "Adding relationship from #{source.to_s} to 
#{target.to_s} with '#{param_name}'"
+        source_resource[param_name] ||= []
+        source_resource[param_name] << target.to_s
+    end
+end
diff --git a/spec/integration/parser/parser.rb 
b/spec/integration/parser/parser.rb
index 5a30f06..ee476c0 100755
--- a/spec/integration/parser/parser.rb
+++ b/spec/integration/parser/parser.rb
@@ -3,6 +3,76 @@
 require File.dirname(__FILE__) + '/../../spec_helper'
 
 describe Puppet::Parser::Parser do
+    module ParseMatcher
+        class ParseAs
+            def initialize(klass)
+                @parser = Puppet::Parser::Parser.new "development"
+                @class = klass
+            end
+
+            def result_instance
+                @result.hostclass("").code[0]
+            end
+
+            def matches?(string)
+                @string = string
+                @result = @parser.parse(string)
+                return result_instance.instance_of?(@class)
+            end
+
+            def description
+                "parse as a #...@class}"
+            end
+
+            def failure_message
+                " expected #...@string} to parse as #...@class} but was 
#{result_instance.class}"
+            end
+
+            def negative_failure_message
+                " expected #...@string} not to parse as #...@class}"
+            end
+        end
+
+        def parse_as(klass)
+            ParseAs.new(klass)
+        end
+
+        class ParseWith
+            def initialize(block)
+                @parser = Puppet::Parser::Parser.new "development"
+                @block = block
+            end
+
+            def result_instance
+                @result.hostclass("").code[0]
+            end
+
+            def matches?(string)
+                @string = string
+                @result = @parser.parse(string)
+                return @block.call(result_instance)
+            end
+
+            def description
+                "parse with the block evaluating to true"
+            end
+
+            def failure_message
+                " expected #...@string} to parse with a true result in the 
block"
+            end
+
+            def negative_failure_message
+                " expected #...@string} not to parse with a true result in the 
block"
+            end
+        end
+
+        def parse_with(&block)
+            ParseWith.new(block)
+        end
+    end
+
+    include ParseMatcher
+
     before :each do
         @resource_type_collection = Puppet::Resource::TypeCollection.new("env")
         @parser = Puppet::Parser::Parser.new "development"
@@ -18,4 +88,26 @@ describe Puppet::Parser::Parser do
             ast.hostclass("test").doc.should == "comment\n"
         end
     end
+
+    describe "when parsing" do
+        it "should be able to parse normal left to right relationships" do
+            "Notify[foo] -> Notify[bar]".should 
parse_as(Puppet::Parser::AST::Relationship)
+        end
+
+        it "should be able to parse right to left relationships" do
+            "Notify[foo] <- Notify[bar]".should 
parse_as(Puppet::Parser::AST::Relationship)
+        end
+
+        it "should be able to parse normal left to right subscriptions" do
+            "Notify[foo] ~> Notify[bar]".should 
parse_as(Puppet::Parser::AST::Relationship)
+        end
+
+        it "should be able to parse right to left subscriptions" do
+            "Notify[foo] <~ Notify[bar]".should 
parse_as(Puppet::Parser::AST::Relationship)
+        end
+
+        it "should correctly set the arrow type of a relationship" do
+            "Notify[foo] <~ Notify[bar]".should parse_with { |rel| rel.arrow 
== "<~" }
+        end
+    end
 end
diff --git a/spec/unit/parser/ast/relationship.rb 
b/spec/unit/parser/ast/relationship.rb
new file mode 100644
index 0000000..acb46e4
--- /dev/null
+++ b/spec/unit/parser/ast/relationship.rb
@@ -0,0 +1,88 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+describe Puppet::Parser::AST::Relationship do
+    before do
+        @class = Puppet::Parser::AST::Relationship
+    end
+
+    it "should set its 'left' and 'right' arguments accordingly" do
+        dep = @class.new(:left, :right, '->')
+        dep.left.should == :left
+        dep.right.should == :right
+    end
+
+    it "should set its arrow to whatever arrow is passed" do
+        @class.new(:left, :right, '->').arrow.should == '->'
+    end
+
+    it "should set its type to :relationship if the relationship type is '<-'" 
do
+        @class.new(:left, :right, '<-').type.should == :relationship
+    end
+
+    it "should set its type to :relationship if the relationship type is '->'" 
do
+        @class.new(:left, :right, '->').type.should == :relationship
+    end
+
+    it "should set its type to :subscription if the relationship type is '~>'" 
do
+        @class.new(:left, :right, '~>').type.should == :subscription
+    end
+
+    it "should set its type to :subscription if the relationship type is '<~'" 
do
+        @class.new(:left, :right, '<~').type.should == :subscription
+    end
+
+    it "should set its line and file if provided" do
+        dep = @class.new(:left, :right, '->', :line => 50, :file => "/foo")
+        dep.line.should == 50
+        dep.file.should == "/foo"
+    end
+
+    describe "when evaluating" do
+        before do
+            @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foo"))
+            @scope = Puppet::Parser::Scope.new(:compiler => @compiler)
+        end
+
+        it "should create a relationship with the evaluated source and target 
and add it to the scope" do
+            source = stub 'source', :safeevaluate => :left
+            target = stub 'target', :safeevaluate => :right
+            @class.new(source, target, '->').evaluate(@scope)
+            @compiler.relationships[0].source.should == :left
+            @compiler.relationships[0].target.should == :right
+        end
+
+        describe "a chained relationship" do
+            before do
+                @left = stub 'left', :safeevaluate => :left
+                @middle = stub 'middle', :safeevaluate => :middle
+                @right = stub 'right', :safeevaluate => :right
+                @first = @class.new(@left, @middle, '->')
+                @second = @class.new(@first, @right, '->')
+            end
+
+            it "should evaluate the relationship to the left" do
+                @first.expects(:evaluate).with(@scope).returns 
Puppet::Parser::Relationship.new(:left, :right, :relationship)
+
+                @second.evaluate(@scope)
+            end
+
+            it "should use the right side of the left relationship as its 
source" do
+                @second.evaluate(@scope)
+
+                @compiler.relationships[0].source.should == :left
+                @compiler.relationships[0].target.should == :middle
+                @compiler.relationships[1].source.should == :middle
+                @compiler.relationships[1].target.should == :right
+            end
+
+            it "should only evaluate a given AST node once" do
+                @left.expects(:safeevaluate).once.returns :left
+                @middle.expects(:safeevaluate).once.returns :middle
+                @right.expects(:safeevaluate).once.returns :right
+                @second.evaluate(@scope)
+            end
+        end
+    end
+end
diff --git a/spec/unit/parser/compiler.rb b/spec/unit/parser/compiler.rb
index 6fd4d1f..c36113f 100755
--- a/spec/unit/parser/compiler.rb
+++ b/spec/unit/parser/compiler.rb
@@ -134,7 +134,7 @@ describe Puppet::Parser::Compiler do
 
         def compile_methods
             [:set_node_parameters, :evaluate_main, :evaluate_ast_node, 
:evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated,
-                :finish, :store, :extract]
+                :finish, :store, :extract, :evaluate_relationships]
         end
 
         # Stub all of the main compile methods except the ones we're 
specifically interested in.
@@ -394,6 +394,15 @@ describe Puppet::Parser::Compiler do
         end
     end
 
+    describe "when evaluating relationships" do
+        it "should evaluate each relationship with its catalog" do
+            dep = stub 'dep'
+            dep.expects(:evaluate).with(@compiler.catalog)
+            @compiler.add_relationship dep
+            @compiler.evaluate_relationships
+        end
+    end
+
     describe "when told to evaluate missing classes" do
 
         it "should fail if there's no source listed for the scope" do
diff --git a/spec/unit/parser/lexer.rb b/spec/unit/parser/lexer.rb
index 2e58ef4..b1524f1 100755
--- a/spec/unit/parser/lexer.rb
+++ b/spec/unit/parser/lexer.rb
@@ -174,6 +174,10 @@ describe Puppet::Parser::Lexer::TOKENS do
         :RSHIFT => '>>',
         :MATCH => '=~',
         :NOMATCH => '!~',
+        :IN_EDGE => '->',
+        :OUT_EDGE => '<-',
+        :IN_EDGE_SUB => '~>',
+        :OUT_EDGE_SUB => '<~',
     }.each do |name, string|
         it "should have a token named #{name.to_s}" do
             Puppet::Parser::Lexer::TOKENS[name].should_not be_nil
diff --git a/spec/unit/parser/relationship.rb b/spec/unit/parser/relationship.rb
new file mode 100644
index 0000000..bd4ff56
--- /dev/null
+++ b/spec/unit/parser/relationship.rb
@@ -0,0 +1,70 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+require 'puppet/parser/relationship'
+
+describe Puppet::Parser::Relationship do
+    before do
+        @source = Puppet::Resource.new(:mytype, "source")
+        @target = Puppet::Resource.new(:mytype, "target")
+        @dep = Puppet::Parser::Relationship.new(@source, @target, 
:relationship)
+    end
+
+    describe "when evaluating" do
+        before do
+            @catalog = Puppet::Resource::Catalog.new
+            @catalog.add_resource(@source)
+            @catalog.add_resource(@target)
+        end
+
+        it "should fail if the source resource cannot be found" do
+            @catalog = Puppet::Resource::Catalog.new
+            @catalog.add_resource @target
+            lambda { @dep.evaluate(@catalog) }.should 
raise_error(ArgumentError)
+        end
+
+        it "should fail if the target resource cannot be found" do
+            @catalog = Puppet::Resource::Catalog.new
+            @catalog.add_resource @source
+            lambda { @dep.evaluate(@catalog) }.should 
raise_error(ArgumentError)
+        end
+
+        it "should add the target as a 'before' value if the type is 
'relationship'" do
+            @dep.type = :relationship
+            @dep.evaluate(@catalog)
+            @source[:before].should be_include("Mytype[target]")
+        end
+
+        it "should add the target as a 'notify' value if the type is 
'subscription'" do
+            @dep.type = :subscription
+            @dep.evaluate(@catalog)
+            @source[:notify].should be_include("Mytype[target]")
+        end
+
+        it "should supplement rather than clobber existing relationship 
values" do
+            @source[:before] = "File[/bar]"
+            @dep.evaluate(@catalog)
+            @source[:before].should be_include("Mytype[target]")
+            @source[:before].should be_include("File[/bar]")
+        end
+
+        it "should use the collected retargets if the target is a Collector" do
+            orig_target = @target
+            @target = Puppet::Parser::Collector.new(stub("scope"), :file, 
"equery", "vquery", :virtual)
+            @target.collected[:foo] = @target
+            @dep.evaluate(@catalog)
+
+            @source[:before].should be_include("Mytype[target]")
+        end
+
+        it "should use the collected resources if the source is a Collector" do
+            orig_source = @source
+            @source = Puppet::Parser::Collector.new(stub("scope"), :file, 
"equery", "vquery", :virtual)
+            @source.collected[:foo] = @source
+            @dep.evaluate(@catalog)
+
+            orig_source[:before].should be_include("Mytype[target]")
+        end
+    end
+end
-- 
1.6.1

-- 
You received this message because you are subscribed to the Google Groups 
"Puppet Developers" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/puppet-dev?hl=en.

Reply via email to