You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@buildr.apache.org by vb...@apache.org on 2008/04/01 23:02:12 UTC

svn commit: r643563 - in /incubator/buildr/trunk: lib/buildr/core/project.rb lib/buildr/java/artifact.rb lib/buildr/java/artifact_namespace.rb spec/artifact_namespace_spec.rb

Author: vborja
Date: Tue Apr  1 14:02:10 2008
New Revision: 643563

URL: http://svn.apache.org/viewvc?rev=643563&view=rev
Log:
BUILDR-4

Added an example for extension authors using ArtifactNamespace and ArtifactRequirement.add_listener
More documentation, specs.

Modified:
    incubator/buildr/trunk/lib/buildr/core/project.rb
    incubator/buildr/trunk/lib/buildr/java/artifact.rb
    incubator/buildr/trunk/lib/buildr/java/artifact_namespace.rb
    incubator/buildr/trunk/spec/artifact_namespace_spec.rb

Modified: incubator/buildr/trunk/lib/buildr/core/project.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/trunk/lib/buildr/core/project.rb?rev=643563&r1=643562&r2=643563&view=diff
==============================================================================
--- incubator/buildr/trunk/lib/buildr/core/project.rb (original)
+++ incubator/buildr/trunk/lib/buildr/core/project.rb Tue Apr  1 14:02:10 2008
@@ -684,7 +684,7 @@
   #
   #     before_define do |project|
   #       # Define the loc task for this particular project.
-  #       define_task 'loc' do |task|
+  #       Rake::Task.define_task 'loc' do |task|
   #         lines = task.prerequisites.map { |path| Dir['#{path}/**/*'] }.flatten.uniq.
   #           inject(0) { |total, file| total + File.readlines(file).count }
   #         puts "Project #{project.name} has #{lines} lines of code"

Modified: incubator/buildr/trunk/lib/buildr/java/artifact.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/trunk/lib/buildr/java/artifact.rb?rev=643563&r1=643562&r2=643563&view=diff
==============================================================================
--- incubator/buildr/trunk/lib/buildr/java/artifact.rb (original)
+++ incubator/buildr/trunk/lib/buildr/java/artifact.rb Tue Apr  1 14:02:10 2008
@@ -570,20 +570,18 @@
 
   # :call-seq:
   #   artifacts(*spec) => artifacts
-  #   artifacts { |namespace| ... } => namespace
-  #   artifacts(scope) { |namespace| ... } => namespace
-  #   artifacts[scope] => namespace
   #
   # Handles multiple artifacts at a time. This method is the plural equivalent of
   # #artifacts, but can do more things.
   #
-  # The first form returns an array of artifacts built using the supplied
+  # Returns an array of artifacts built using the supplied
   # specifications, each of which can be:
   # * An artifact specification (String or Hash). Returns the appropriate Artifact task.
   # * An artifact of any other task. Returns the task as is.
   # * A project. Returns all artifacts created (packaged) by that project.
   # * A string. Returns that string, assumed to be a file name.
   # * An array of artifacts or a Struct.
+  # * A symbol. Returns the named artifact from the current ArtifactNamespace
   #
   # For example, handling a collection of artifacts:
   #   xml = [ xerces, xalan, jaxp ]
@@ -594,10 +592,6 @@
   # Using artifacts created by a project:
   #   artifacts project('my-app')               # All packages
   #   artifacts project('my-app').package(:war) # Only the WAR
-  #
-  # Last three forms return an ArtifactNamespace instead of an array
-  # and are only useful to work with the resulting namespace. See the
-  # documentation for ArtifactNamespace for more info.
   def artifacts(*specs, &block)
     specs.flatten.inject([]) do |set, spec|
       case spec
@@ -645,7 +639,7 @@
   end
 
   # :call-seq:
-  #   groups(ids, :under=>group_name, :version=>number) => artifacts
+  #   group(ids, :under=>group_name, :version=>number) => artifacts
   #
   # Convenience method for defining multiple artifacts that belong to the same group and version.
   # Accepts multiple artifact identifiers follows by two hash values:

Modified: incubator/buildr/trunk/lib/buildr/java/artifact_namespace.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/trunk/lib/buildr/java/artifact_namespace.rb?rev=643563&r1=643562&r2=643563&view=diff
==============================================================================
--- incubator/buildr/trunk/lib/buildr/java/artifact_namespace.rb (original)
+++ incubator/buildr/trunk/lib/buildr/java/artifact_namespace.rb Tue Apr  1 14:02:10 2008
@@ -85,7 +85,7 @@
   #
   #     # When a child namespace asks for the :log artifact, 
   #     # these artifacts will be searched starting from the :current namespace.
-  #     ns.dynamic :log, :logger, :commons_logging
+  #     ns.virtual :log, :logger, :commons_logging
   #   end
   #   
   #   artifact_ns('example2:one') do |ns| # namespace for the one subproject
@@ -195,7 +195,9 @@
   #       end
   #
   #    end 
-  # 
+  #
+  # A more advanced example using ArtifactRequirement listeners is included 
+  # in the artifact_namespace_spec.rb description for 'Extension using ArtifactNamespace'
   # That's it for addon writers, now, users can select their prefered version with
   # something like:
   #
@@ -219,7 +221,7 @@
       def clear
         @instances = nil
         remove_const(:ROOT) if const_defined?(:ROOT)
-        const_set(:ROOT, new(:root))
+        const_set(:ROOT, new('root'))
       end
 
       # Populate namespaces from a hash of hashes. 
@@ -227,8 +229,8 @@
       #
       #   -- profiles.yaml --
       #   development:
-      #     :artifacts:
-      #       :root:        # root namespace
+      #     artifacts:
+      #       root:        # root namespace
       #         spring:     org.springframework:spring:jar:2.5
       #         groovy:     org.codehaus.groovy:groovy:jar:1.5.4
       #         logging:    # define a named group
@@ -244,7 +246,7 @@
       #         spring:  org.springframework:spring:jar:1.0
       #
       #   -- buildfile --
-      #   ArtifactNamespace.load(Buildr.profile[:artifacts])
+      #   ArtifactNamespace.load(Buildr.profile['artifacts'])
       def load(namespaces = {})
         namespaces.each_pair { |name, uses| instance(name).use(uses) }
       end
@@ -271,9 +273,9 @@
                  when nil then Rake.application.current_scope.join(':')
                  end
         end
-        name = name.to_s.split(/:{2,}/).join(':')
+        name = name.to_s
         return ROOT if name.size == 0
-        name = name.intern
+        name = name.to_s
         @instances ||= Hash.new { |h, k| h[k] = new(k) }
         instance = @instances[name]
         yield(instance) if block_given?
@@ -412,7 +414,7 @@
       end
 
       def name=(name)
-        @name = name.to_s.to_sym
+        @name = name.to_s
       end
       
       # Set a the requirement, this must be an string formatted for
@@ -467,9 +469,14 @@
 
       def selected! #:nodoc:
         @selected = true
+        @listeners.each { |l| l.call(self) } if @listeners
         self
       end
 
+      def add_listener(&callback)
+        (@listeners ||= []) << callback
+      end
+
       # Return the Artifact object for the currently selected version
       def artifact
         ::Buildr.artifact(self)
@@ -504,8 +511,8 @@
     include Enumerable
     attr_reader :name
     
-    def initialize(name) #:nodoc:
-      @name = name.to_sym
+    def initialize(name = nil) #:nodoc:
+      @name = name.to_s if name
     end
     clear
     
@@ -515,7 +522,18 @@
     
     # ROOT namespace has no parent
     def parent
-      root? ? nil : ArtifactNamespace.instance(@parent || @name.to_s.split(':')[0...-1].join(':'))
+      if root?
+        nil
+      elsif @parent.kind_of?(ArtifactNamespace)
+        @parent
+      elsif @parent
+        ArtifactNamespace.instance(@parent)
+      elsif name
+        parent_name = name.gsub(/::?[^:]+$/, '')
+        parent_name == name ? root : ArtifactNamespace.instance(parent_name)
+      else
+        root
+      end
     end
     
     # Set the parent for the current namespace, except if it is ROOT
@@ -536,7 +554,7 @@
     # Reference needs to be through this object using the given +name+
     #
     #   artifact_ns('foo').ns(:bar).need :thing => 'some:thing:jar:1.0'
-    #   artifact_ns('foo').bar # => the sub-namespace
+    #   artifact_ns('foo').bar # => the sub-namespace 'foo.bar'
     #   artifact_ns('foo').bar.thing # => the some thing artifact
     #
     # See the top level ArtifactNamespace documentation for examples
@@ -555,6 +573,12 @@
       sub
     end
 
+    # Test if a sub-namespace by the given name exists
+    def ns?(name)
+      sub = registry[name.to_sym]
+      ArtifactNamespace === sub
+    end
+
     # :call-seq:
     #   artifact_ns.need 'name -> org:foo:bar:jar:~>1.2.3 -> 1.2.5'
     #   artifact_ns.need :name => 'org.foo:bar:jar:1.0'
@@ -754,12 +778,24 @@
     # Return only the named requirements
     def values_at(*names)
       names.map do |name| 
-        name = name.to_s[/^\w+$/] ? name : ArtifactRequirement.unversioned_spec(name)
-        get(name.to_sym)
+        catch :artifact do
+          unless name.to_s[/^\w+$/] 
+            unvers = ArtifactRequirement.unversioned_spec(name)
+            unless unvers.to_s == name.to_s
+              req = ArtifactRequirement.new(name)
+              reg = self
+              while reg
+                candidate = reg.send(:get, unvers, false, false, true)
+                throw :artifact, candidate if req.satisfied_by?(candidate)
+                reg = reg.parent
+              end
+            end
+          end
+          get(name.to_sym)
+        end
       end
     end
 
-
     def key?(name, include_parents = false)
       name = ArtifactRequirement.unversioned_spec(name) unless name.to_s[/^\w+$/]
       registry.key?(name, include_parents)
@@ -789,7 +825,7 @@
       self
     end
 
-    alias_method :dynamic, :group
+    alias_method :virtual, :group
 
     # Create an alias for a named requirement.
     def alias(new_name, old_name)
@@ -841,7 +877,31 @@
       when /\?$/ then
         name = $`.gsub(/^(has|have)_/, '').intern
         [get(name)].flatten.all? { |a| a && a.selected? }
-      else get(name)
+      else 
+        if block || args.size > 0
+          raise ArgumentError.new("wrong number of arguments #{args.size} for 0 or block given")
+        end
+        get(name)
+      end
+    end
+
+    # Return an anonymous module 
+    #   # first create a requirement
+    #   artifact_ns.cool_aid! 'cool:aid:jar:>=1.0'
+    #
+    #   # extend an object as a cool_aid delegator
+    #   jars = Object.new.extend(artifact_ns.accessor(:cool_aid))
+    #   jars.cool_aid = '2.0'
+    #
+    #   artifact_ns.cool_aid.version # -> '2.0'
+    def accessor(*names)
+      ns = self
+      Module.new do 
+        names.each do |name|
+          define_method("#{name}") { ns.send("#{name}") }
+          define_method("#{name}?") { ns.send("#{name}?") }
+          define_method("#{name}=") { |vers| ns.send("#{name}=", vers) }
+        end
       end
     end
 
@@ -878,13 +938,17 @@
      
   end # ArtifactNamespace
 
+  # :call-seq:
+  #   project.artifact_ns -> ArtifactNamespace
+  #   Buildr.artifact_ns(name) -> ArtifactNamespace
+  #   Buildr.artifact_ns -> ArtifactNamespace for the currently running Project
+  #
   # Open an ArtifactNamespace.
-  # If no name is given, the namespace for the currently running 
-  # project is returned. If a block is provided, the namespace
-  # is yielded to it.
+  # If a block is provided, the namespace is yielded to it.
   #
   # See also ArtifactNamespace.instance
   def artifact_ns(name = nil, &block)
+    name = self if name.nil? && self.kind_of?(Project)
     ArtifactNamespace.instance(name, &block)
   end
   

Modified: incubator/buildr/trunk/spec/artifact_namespace_spec.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/trunk/spec/artifact_namespace_spec.rb?rev=643563&r1=643562&r2=643563&view=diff
==============================================================================
--- incubator/buildr/trunk/spec/artifact_namespace_spec.rb (original)
+++ incubator/buildr/trunk/spec/artifact_namespace_spec.rb Tue Apr  1 14:02:10 2008
@@ -13,11 +13,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
-
 require File.join(File.dirname(__FILE__), 'spec_helpers')
 
-require 'buildr/java/artifact_namespace'
-
 describe Buildr::ArtifactNamespace do
 
   before(:each) { Buildr::ArtifactNamespace.clear }
@@ -43,14 +40,19 @@
     it 'should return the top level namespace when invoked outside a project definition' do
       artifact_ns.should be_root
     end
+
+    it 'should return the namespace for the receiving project' do 
+      define('foo') { }
+      project('foo').artifact_ns.name.should == 'foo'
+    end
     
     it 'should return the current project namespace when invoked inside a project' do
       define 'foo' do
         artifact_ns.should_not be_root
-        artifact_ns.name.should == :foo
+        artifact_ns.name.should == 'foo'
         task :doit do 
           artifact_ns.should_not be_root
-          artifact_ns.name.should == :foo
+          artifact_ns.name.should == 'foo'
         end.invoke
       end
     end
@@ -60,18 +62,18 @@
     end
     
     it 'should return the namespace for the given name' do 
-      artifact_ns(:foo).name.should == :foo
-      artifact_ns('foo:bar').name.should == 'foo:bar'.intern
-      artifact_ns(['foo', 'bar', 'baz']).name.should == 'foo:bar:baz'.intern
+      artifact_ns(:foo).name.should == 'foo'
+      artifact_ns('foo:bar').name.should == 'foo:bar'
+      artifact_ns(['foo', 'bar', 'baz']).name.should == 'foo:bar:baz'
       abc_module do 
-        artifact_ns(A::B::C).name.should == 'A:B:C'.intern
+        artifact_ns(A::B::C).name.should == 'A::B::C'
       end
       artifact_ns(:root).should be_root
       artifact_ns(:current).should be_root
       define 'foo' do
-        artifact_ns(:current).name.should == :foo
+        artifact_ns(:current).name.should == 'foo'
         define 'baz' do
-          artifact_ns(:current).name.should == 'foo:baz'.intern
+          artifact_ns(:current).name.should == 'foo:baz'
         end
       end
     end
@@ -287,6 +289,54 @@
       end
     end
   end
+
+  describe '#values_at' do 
+    it 'returns the named artifacts' do 
+      define 'foo' do
+        artifact_ns.use 'foo:one:baz:1.0'
+        define 'bar' do
+          artifact_ns.use :foo_baz => 'foo:two:baz:1.0'
+          
+          specs = artifact_ns.values_at('one').map(&:to_spec)
+          specs.should include('foo:one:baz:1.0')
+          specs.should_not include('foo:two:baz:1.0')
+
+          specs = artifact_ns.values_at('foo_baz').map(&:to_spec)
+          specs.should include('foo:two:baz:1.0')
+          specs.should_not include('foo:one:baz:1.0')
+        end
+      end
+    end
+
+    it 'returns first artifacts by their unversioned spec' do
+      define 'foo' do
+        artifact_ns.use 'foo:one:baz:2.0'
+        define 'bar' do
+          artifact_ns.use :older => 'foo:one:baz:1.0'
+          
+          specs = artifact_ns.values_at('foo:one:baz').map(&:to_spec)
+          specs.should include('foo:one:baz:1.0')
+          specs.should_not include('foo:one:baz:2.0')
+        end
+        specs = artifact_ns.values_at('foo:one:baz').map(&:to_spec)
+        specs.should include('foo:one:baz:2.0')
+        specs.should_not include('foo:one:baz:1.0')
+      end
+    end
+
+    it 'return first artifact satisfying a dependency' do
+      define 'foo' do
+        artifact_ns.use 'foo:one:baz:2.0'
+        define 'bar' do
+          artifact_ns.use :older => 'foo:one:baz:1.0'
+          
+          specs = artifact_ns.values_at('foo:one:baz:>1.0').map(&:to_spec)
+          specs.should include('foo:one:baz:2.0')
+          specs.should_not include('foo:one:baz:1.0')
+        end
+      end
+    end
+  end
   
   describe '#method_missing' do 
     it 'should use cool_aid! to create a requirement' do 
@@ -442,6 +492,144 @@
           lambda { artifact(:cool) }.should raise_error(IndexError, /artifact/)
         end
       end
+    end
+  end
+end
+
+describe "Extension using ArtifactNamespace" do
+  before(:each) { Buildr::ArtifactNamespace.clear }
+
+  def abc_module
+    Object.module_eval 'module A; module B; module C; end; end; end'
+    yield
+  ensure
+    Object.send :remove_const, :A
+  end
+  
+  it 'can register namespace listeners' do
+    abc_module do
+      # An example extension to illustrate namespace listeners and method forwarding
+      class A::Example
+        
+        module Ext
+          include Buildr::Extension
+          def example; @example ||= A::Example.new; end
+          before_define do |p|
+            Rake::Task.define_task('example') { p.example.doit }
+          end
+        end
+        
+        REQUIRES = ArtifactNamespace.for(self) do |ns|
+          ns.xmlbeans! 'org.apache.xmlbeans:xmlbeans:jar:2.3.0', '>2'
+          ns.stax_api! 'stax:stax-api:jar:>=1.0.1'
+        end
+
+        attr_reader :options, :requires
+        
+        def initialize
+          # We could actually use the REQUIRES namespace, but to make things
+          # a bit more interesting, suppose each Example instance can have its 
+          # own artifact requirements in adition to those specified on REQUIRES.
+          # To achieve this we create an anonymous namespace.
+          @requires = ArtifactNamespace.new # a namespace per instance
+          REQUIRES.each { |requirement| @requires.need requirement }
+          
+          # For user convenience, we make the options object respond to
+          #    :xmlbeans, :xmlbeans=, :xmlbeans?
+          # forwarding them to the namespace.
+          @options = OpenObject.new.extend(@requires.accessor(:xmlbeans, :stax_api))
+          # Register callbacks so we can perform some logic when an artifact
+          # is selected by the user.
+          options.xmlbeans.add_listener &method(:selected_xmlbeans)
+          options.stax_api.add_listener do |stax|
+            # Now using a proc
+            stax.should be_selected
+            stax.version.should == '1.6180'
+            options[:math] = :golden # customize our options for this version
+            # In this example we set the stax version when running outside
+            # a project definition. This means we have no access to the project
+            # namespace unless we had a reference to the project or knew it's name
+            Buildr.artifact_ns(:current).name.should == 'root'
+          end
+        end
+
+        include Spec::Matchers # for assertions
+        
+        # Called with the ArtifactRequirement that has just been selected
+        # by a user. This allows extension author to selectively perform
+        # some action by inspecting the requirement state.
+        def selected_xmlbeans(xmlbeans)
+          xmlbeans.should be_selected
+          xmlbeans.version.should == '3.1415'
+          options[:math] = :pi
+          # This example just sets xmlbeans for foo:bar project
+          # So the currently running namespace should have the foo:bar name
+          Buildr.artifact_ns(:current).name.should == 'foo:bar'
+        end
+
+        # Suppose we invoke an ant task here or something else.
+        def doit
+          # Now call ant task with our selected artifact and options
+          classpath = requires.map(&:artifact).map(&:to_s).join(File::PATH_SEPARATOR)
+          lambda { ant('thing') { |ant| ant.classpath classpath, :math => options[:math] } }
+          
+          # We are not a Project instance, hence we have no artifact_ns
+          lambda { artifact_ns }.should raise_error(NameError)
+
+          # Extension authors may NOT rely project's namespaces.
+          # However the ruby-way gives you power and at the same time 
+          # makes you dangerous, (think open-modules, monkey-patching)
+          # Given that buildr is pure ruby, consider it a sharp-edged sword.
+          # Having said that, you may actually inspect a project's 
+          # namespace, but don't write on it without letting your users
+          # know you will.
+          # This example obtains the current project namespace to make
+          # some assertions.
+          
+          # To obtain a project's namespace we need either
+          # 1) a reference to the project, and call artifact_ns on it
+          #      project.artifact_ns  # the namespace for project
+          # 2) know the project name
+          #      Buildr.artifact_ns('the:project')
+          # 3) Use :current to reference the currently running project
+          #      Buildr.artifact_ns(:current)
+          name = Buildr.artifact_ns(:current).name
+          case name
+          when 'foo:bar'
+            options[:math].should == :pi
+            requires.xmlbeans.version.should == '3.1415'
+            requires.stax_api.version.should == '1.0.1'
+          when 'foo:baz'
+            options[:math].should == :golden
+            requires.xmlbeans.version.should == '2.3.0'
+            requires.stax_api.version.should == '1.6180'
+          else
+            fail "This example expects foo:bar or foo:baz projects not #{name.inspect}"
+          end
+        end
+      end
+      
+      define 'foo' do
+        define 'bar' do
+          extend A::Example::Ext
+          task('setup') do
+            example.options.xmlbeans = '3.1415'
+          end
+          task('run' => [:setup, :example])
+        end
+        define 'baz' do
+          extend A::Example::Ext
+        end
+      end
+
+      project('foo:bar').example.requires.should_not == project('foo:baz').example.requires
+      project('foo:bar').example.requires.xmlbeans.should_not == project('foo:baz').example.requires.xmlbeans
+      
+      # current namespace outside a project is :root, see the stax callback
+      project('foo:baz').example.options.stax_api = '1.6180'
+      # we call the task outside the project, see #doit
+      lambda { task('foo:bar:run').invoke }.should run_task('foo:bar:example')
+      lambda { task('foo:baz:example').invoke }.should run_task('foo:baz:example')
     end
   end
 end