incubator-deltacloud-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From tcraw...@redhat.com
Subject [PATCH] Report what operations are available from api and from the client.
Date Fri, 21 Jan 2011 18:29:53 GMT
From: Tobias Crawley <tcrawley@redhat.com>

Changes to the server:

This provides capability reporting to the /api and the client. The /api xml format 
now looks like:

  <link href='http://localhost:3001/api/instances' rel='instances'> 
    <feature name='hardware_profiles'></feature> 
    <feature name='user_name'></feature> 
    <feature name='authentication_key'></feature> 
    <operation method='post' name='reboot' scope='member'> 
    </operation> 
    <operation method='get' name='show' scope='member'> 
      <param feature='authentication_key' /> 
    </operation> 
    <operation method='post' name='start' scope='member'> 
    </operation> 
    <operation method='delete' name='destroy' scope='member'> 
    </operation> 
    <operation method='post' name='stop' scope='member'> 
    </operation> 
    <operation method='post' name='create' scope='collection'> 
      <param feature='hardware_profiles' /> 
      <param feature='user_name' /> 
      <param feature='authentication_key' /> 
    </operation> 
    <operation method='get' name='index' scope='collection'> 
    </operation> 
  </link> 

I tested this new format with the existing (0.1.0) ruby client, the JBDS java client,
and libdeltacloud and they were able to parse it and function w/o an issue. The new 
format differs slightly from the format we discussed earlier on the list - the nested 
<feature> elements under <operation> broke the ruby client. I'm not attached to
<param> 
for that element, and am open to suggestions.

Changes to the client:

The client will only define the collection methods (like client.instances) if an 
:index operation is defined for the collection. It will also only define the member
methods (like client.instance, client.fetch_instance) if a :show operation is 
defined. The client will only respond to the create_* methods (like 
client.create_instance) if the :create operation is defined.

If no operations are defined (meaning that the client is talking to an older, 0.1 api),
it falls back and acts just like a 0.1 client.

The client has new methods for accessing the operation/feature information:

allow_operation?(collection, operation) - returns true if the operation is allowed 
on that collection, or if no operations are defined (meaning a 0.1 api).

features_for_operation(collection, operation) - returns an array of features for a 
given operation on a collection. Raises an ArgumentError if called when no 
operations are defined (meaning a 0.1 api), since no useful answer can be returned.

There is currently no enforcement that actions on member objects are legal operations - 
that burden is still on the api when setting the actions available on the returned
member. There is also no feature validation.

---
 .gitignore                           |    1 +
 client/lib/deltacloud.rb             |  174 +++++++++++++++++---------
 client/lib/string.rb                 |    7 +
 client/specs/fixtures/api/v1_api.xml |   23 ++++
 client/specs/fixtures/api/v2_api.xml |   91 ++++++++++++++
 client/specs/initialization_spec.rb  |  229 ++++++++++++++++++++++++++++++++++
 client/specs/spec_helper.rb          |   10 +-
 server/lib/sinatra/rabbit.rb         |    8 ++
 server/views/api/show.xml.haml       |    4 +
 9 files changed, 486 insertions(+), 61 deletions(-)
 create mode 100644 client/specs/fixtures/api/v1_api.xml
 create mode 100644 client/specs/fixtures/api/v2_api.xml

diff --git a/.gitignore b/.gitignore
index 1ee84da..8062fda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.sw*
+pkg
diff --git a/client/lib/deltacloud.rb b/client/lib/deltacloud.rb
index aac136a..d48aeb8 100644
--- a/client/lib/deltacloud.rb
+++ b/client/lib/deltacloud.rb
@@ -63,7 +63,7 @@ module DeltaCloud
   end
 
   class API
-    attr_reader :api_uri, :driver_name, :api_version, :features, :entry_points
+    attr_reader :api_uri, :driver_name, :api_version, :features, :entry_points, :operations
     attr_reader :api_driver, :api_provider
 
     def initialize(user_name, password, api_url, opts={}, &block)
@@ -71,7 +71,7 @@ module DeltaCloud
       @api_driver, @api_provider = opts[:driver], opts[:provider]
       @username, @password = opts[:username] || user_name, opts[:password] || password
       @api_uri = URI.parse(api_url)
-      @features, @entry_points = {}, {}
+      clear_entry_points
       @verbose = opts[:verbose] || false
       discover_entry_points
       yield self if block_given?
@@ -108,7 +108,7 @@ module DeltaCloud
       yield api_instance if block_given?
       api_instance
     end
-
+    
     def connect(&block)
       yield self
     end
@@ -124,32 +124,51 @@ module DeltaCloud
 
     # Define methods based on 'rel' attribute in entry point
     # Two methods are declared: 'images' and 'image'
-    def declare_entry_points_methods(entry_points)
-
-      API.instance_eval do
-        entry_points.keys.select {|k| [:instance_states].include?(k)==false }.each do |model|
-
-          define_method model do |*args|
-            request(:get, entry_points[model], args.first) do |response|
-              base_object_collection(model, response)
+    def declare_entry_points_methods
+      # store ref to this object
+      this = self
+      # then operate on the metaclass so we're only defining these
+      # methods on this object
+      metaclass.instance_eval do
+        this.entry_points.keys.reject { |k| [:instance_states].include?(k) }.each do |model|
+          if this.allow_operation?(model, :index)
+            define_method model do |*args|
+              request(:get, this.entry_points[model], args.first) do |response|
+                base_object_collection(model, response)
+              end
             end
           end
 
-          define_method :"#{model.to_s.singularize}" do |*args|
-            request(:get, "#{entry_points[model]}/#{args[0]}") do |response|
-              base_object(model, response)
+          if this.allow_operation?(model, :show)
+            define_method :"#{model.to_s.singularize}" do |*args|
+              request(:get, "#{this.entry_points[model]}/#{args[0]}") do |response|
+                base_object(model, response)
+              end
             end
-          end
 
-          define_method :"fetch_#{model.to_s.singularize}" do |url|
-            id = url.grep(/\/#{model}\/(.*)$/)
-            self.send(model.to_s.singularize.to_sym, $1)
+            define_method :"fetch_#{model.to_s.singularize}" do |url|
+              id = url.grep(/\/#{model}\/(.*)$/)
+              send(model.to_s.singularize.to_sym, $1)
+            end
           end
-
         end
       end
     end
 
+    def allow_operation?(collection, operation)
+      raise ArgumentError.new(":#{collection} is not a valid collection") unless entry_points.keys.include?(collection)
+      return true if operations.empty? # if we don't know anything about collections, we
allow anything
+      operations[collection] and operations[collection].find { |op| op.name == operation
}
+    end
+
+    def features_for_operation(collection, operation)
+      raise ArgumentError.new("The API does not provide collection specific features") if
operations.empty?
+      raise ArgumentError.new(":#{collection} is not a valid collection") unless entry_points.keys.include?(collection)
+      operation = operations[collection].find { |c| c.name == operation }
+      raise ArgumentError.new(":#{operation} is not a valid operation for :#{collection}")
unless operation
+      operation.features
+    end
+    
     def base_object_collection(model, response)
       Nokogiri::XML(response).xpath("#{model}/#{model.to_s.singularize}").collect do |item|
         base_object(model, item.to_s)
@@ -194,7 +213,7 @@ module DeltaCloud
           end
         end
 
-        # If there are actions, add they to ActionObject/StateFullObject
+        # If there are actions, add them to ActionObject/StateFullObject
         if attribute.name == 'actions'
           (attribute/'link').each do |link|
             obj.add_action_link!(item['id'], link)
@@ -212,16 +231,16 @@ module DeltaCloud
           obj.add_collection!(attribute.name, (attribute/'*').collect { |value| value.text
}) && next
         end
 
-        # Anything else is treaten as text object
+        # Anything else is treated as text object
         obj.add_text!(attribute.name, attribute.text.convert)
       end
 
       return obj
     end
-
+    
     # Get /api and parse entry points
     def discover_entry_points
-      return if discovered?
+      clear_entry_points if discovered?
       request(:get, @api_uri.to_s) do |response|
         api_xml = Nokogiri::XML(response)
         @driver_name = api_xml.xpath('/api').first['driver']
@@ -234,48 +253,31 @@ module DeltaCloud
           entry_point.css("feature").each do |feature|
             @features[rel] ||= []
             @features[rel] << feature['name'].to_sym
+          end
 
+          entry_point.css("operation").each do |operation|
+            @operations[rel] ||= []
+            op = Operation.new(rel, operation['name'], operation['method'], operation['scope'])
+            @operations[rel] << op
+            operation.css("param").each do |param|
+              op.features << param['feature'].to_sym
+            end
           end
         end
       end
-      declare_entry_points_methods(@entry_points)
+      declare_entry_points_methods
     end
 
-    # Generate create_* methods dynamically
+    # Handle create_* methods dynamically
     #
-    def method_missing(name, *args)
-      if name.to_s =~ /create_(\w+)/
-        params = args[0] if args[0] and args[0].class.eql?(Hash)
-        params ||= args[1] if args[1] and args[1].class.eql?(Hash)
-        params ||= {}
-
-        # FIXME: This fixes are related to Instance model and should be
-        # replaced by 'native' parameter names
-
-        params[:realm_id] ||= params[:realm] if params[:realm]
-        params[:keyname] ||= params[:key_name] if params[:key_name]
-
-        if params[:hardware_profile] and params[:hardware_profile].class.eql?(Hash)
-          params[:hardware_profile].each do |k,v|
-            params[:"hwp_#{k}"] ||= v
-          end
-        else
-          params[:hwp_id] ||= params[:hardware_profile]
-        end
-
-        params[:image_id] ||= params[:image_id] || args[0] if args[0].class!=Hash
-
-        obj = nil
-
-        request(:post, entry_points[:"#{$1}s"], {}, params) do |response|
-          obj = base_object(:"#{$1}", response)
-          yield obj if block_given?
-        end
-        return obj
+    def method_missing(name, *args, &block)
+      if name.to_s =~ /create_(\w+)/ && allow_operation?($1.pluralize.to_sym, :create)
+        create_record($1, args)
+      else
+        super
       end
-      raise NoMethodError
     end
-
+    
     def use_driver(driver, opts={})
       if driver
         @api_driver = driver 
@@ -382,7 +384,6 @@ module DeltaCloud
       instance_states.select { |s| s.name.to_s.eql?(name.to_s) }.first
     end
 
-    # Skip parsing /api when we already got entry points
     def discovered?
       true if @entry_points!={}
     end
@@ -426,6 +427,65 @@ module DeltaCloud
       }
     end
 
+    def clear_entry_points
+      this = self
+      metaclass.instance_eval do
+        this.entry_points.keys.reject { |k| [:instance_states].include?(k) }.each do |collection|
+          remove_method(collection) if this.respond_to?(collection)
+          member = collection.to_s.singularize
+          remove_method(member) if this.respond_to?(member)
+          remove_method("fetch_#{member}") if this.respond_to?("fetch_#{member}")
+        end if this.entry_points
+      end
+
+      @entry_points, @features, @operations = {}, {}, {}
+    end
+    
+    def create_record(model, args)
+      params = args[0] if args[0] and args[0].class.eql?(Hash)
+      params ||= args[1] if args[1] and args[1].class.eql?(Hash)
+      params ||= {}
+
+      # FIXME: This fixes are related to Instance model and should be
+      # replaced by 'native' parameter names
+
+      params[:realm_id] ||= params[:realm] if params[:realm]
+      params[:keyname] ||= params[:key_name] if params[:key_name]
+
+      if params[:hardware_profile] and params[:hardware_profile].class.eql?(Hash)
+        params[:hardware_profile].each do |k,v|
+          params[:"hwp_#{k}"] ||= v
+        end
+      else
+        params[:hwp_id] ||= params[:hardware_profile]
+      end
+
+      params[:image_id] ||= params[:image_id] || args[0] if args[0].class!=Hash
+
+      obj = nil
+
+      request(:post, entry_points[:"#{model}s"], {}, params) do |response|
+        obj = base_object(:"#{model}", response)
+        yield obj if block_given?
+      end
+      
+      obj
+    end
+
+    def metaclass
+      class << self; self; end
+    end
   end
 
+  class Operation
+    attr_reader :collection, :name, :method, :scope, :features
+    
+    def initialize(collection, name, method, scope)
+      @collection = collection.to_sym
+      @name = name.to_sym
+      @method = method.to_sym
+      @scope = scope.to_sym
+      @features = []
+    end
+  end
 end
diff --git a/client/lib/string.rb b/client/lib/string.rb
index 72cc259..32fa9e4 100644
--- a/client/lib/string.rb
+++ b/client/lib/string.rb
@@ -39,6 +39,13 @@ class String
     end
   end
 
+  unless method_defined?(:pluralize)
+    # Add 's' character to end of string
+    def pluralize
+      "#{self}s"
+    end
+  end
+
   # Convert string to float if string value seems like Float
   def convert
     return self.to_f if self.strip =~ /^([\d\.]+$)/
diff --git a/client/specs/fixtures/api/v1_api.xml b/client/specs/fixtures/api/v1_api.xml
new file mode 100644
index 0000000..abe6c68
--- /dev/null
+++ b/client/specs/fixtures/api/v1_api.xml
@@ -0,0 +1,23 @@
+<api driver='mock' version='0.1.0'> 
+  <link href='http://localhost:3001/api/storage_volumes' rel='storage_volumes'> 
+  </link> 
+  <link href='http://localhost:3001/api/instance_states' rel='instance_states'> 
+  </link> 
+  <link href='http://localhost:3001/api/instances' rel='instances'> 
+    <feature name='hardware_profiles'></feature> 
+    <feature name='user_name'></feature> 
+    <feature name='authentication_key'></feature> 
+  </link> 
+  <link href='http://localhost:3001/api/storage_snapshots' rel='storage_snapshots'>

+  </link> 
+  <link href='http://localhost:3001/api/keys' rel='keys'> 
+  </link> 
+  <link href='http://localhost:3001/api/hardware_profiles' rel='hardware_profiles'>

+  </link> 
+  <link href='http://localhost:3001/api/images' rel='images'> 
+  </link> 
+  <link href='http://localhost:3001/api/realms' rel='realms'> 
+  </link> 
+  <link href='http://localhost:3001/api/buckets' rel='buckets'> 
+  </link> 
+</api> 
diff --git a/client/specs/fixtures/api/v2_api.xml b/client/specs/fixtures/api/v2_api.xml
new file mode 100644
index 0000000..8ef4f10
--- /dev/null
+++ b/client/specs/fixtures/api/v2_api.xml
@@ -0,0 +1,91 @@
+<api driver='mock' version='0.2.0'> 
+  <link href='http://localhost:3001/api/storage_volumes' rel='storage_volumes'> 
+    <operation method='get' name='show' scope='member'> 
+    </operation> 
+    <operation method='delete' name='destroy' scope='member'> 
+    </operation> 
+    <operation method='post' name='attach' scope='member'> 
+    </operation> 
+    <operation method='post' name='detach' scope='member'> 
+    </operation> 
+    <operation method='post' name='create' scope='collection'> 
+    </operation> 
+    <operation method='get' name='index' scope='collection'> 
+    </operation> 
+  </link> 
+  <link href='http://localhost:3001/api/instance_states' rel='instance_states'> 
+    <operation method='get' name='index' scope='collection'> 
+    </operation> 
+  </link> 
+  <link href='http://localhost:3001/api/instances' rel='instances'> 
+    <feature name='hardware_profiles'></feature> 
+    <feature name='user_name'></feature> 
+    <feature name='authentication_key'></feature> 
+    <operation method='post' name='reboot' scope='member'> 
+    </operation> 
+    <operation method='get' name='show' scope='member'> 
+      <param feature='authentication_key' /> 
+    </operation> 
+    <operation method='post' name='start' scope='member'> 
+    </operation> 
+    <operation method='delete' name='destroy' scope='member'> 
+    </operation> 
+    <operation method='post' name='stop' scope='member'> 
+    </operation> 
+    <operation method='post' name='create' scope='collection'> 
+      <param feature='hardware_profiles' /> 
+      <param feature='user_name' /> 
+      <param feature='authentication_key' /> 
+    </operation> 
+    <operation method='get' name='index' scope='collection'> 
+    </operation> 
+  </link> 
+  <link href='http://localhost:3001/api/storage_snapshots' rel='storage_snapshots'>

+    <operation method='get' name='show' scope='member'> 
+    </operation> 
+    <operation method='delete' name='destroy' scope='member'> 
+    </operation> 
+    <operation method='post' name='create' scope='collection'> 
+    </operation> 
+    <operation method='get' name='index' scope='collection'> 
+    </operation> 
+  </link> 
+  <link href='http://localhost:3001/api/keys' rel='keys'> 
+    <operation method='get' name='show' scope='member'> 
+    </operation> 
+    <operation method='delete' name='destroy' scope='member'> 
+    </operation> 
+    <operation method='post' name='create' scope='collection'> 
+    </operation> 
+    <operation method='get' name='index' scope='collection'> 
+    </operation> 
+  </link> 
+  <link href='http://localhost:3001/api/hardware_profiles' rel='hardware_profiles'>

+    <operation method='get' name='show' scope='member'> 
+    </operation> 
+    <operation method='get' name='index' scope='collection'> 
+    </operation> 
+  </link> 
+  <link href='http://localhost:3001/api/images' rel='images'> 
+    <operation method='get' name='show' scope='member'> 
+    </operation> 
+    <operation method='get' name='index' scope='collection'> 
+    </operation> 
+  </link> 
+  <link href='http://localhost:3001/api/realms' rel='realms'> 
+    <operation method='get' name='show' scope='member'> 
+    </operation> 
+    <operation method='get' name='index' scope='collection'> 
+    </operation> 
+  </link> 
+  <link href='http://localhost:3001/api/buckets' rel='buckets'> 
+    <operation method='get' name='show' scope='member'> 
+    </operation> 
+    <operation method='delete' name='destroy' scope='member'> 
+    </operation> 
+    <operation method='post' name='create' scope='collection'> 
+    </operation> 
+    <operation method='get' name='index' scope='collection'> 
+    </operation> 
+  </link> 
+</api> 
diff --git a/client/specs/initialization_spec.rb b/client/specs/initialization_spec.rb
index 672d858..3ef9723 100644
--- a/client/specs/initialization_spec.rb
+++ b/client/specs/initialization_spec.rb
@@ -59,4 +59,233 @@ describe "initializing the client" do
     end
   end
 
+  describe 'loading operations' do
+    it "should load the operations for each collection" do
+      DeltaCloud.new( "name", "password", API_URL ) do |client|
+        def client.should_have_operations(collection, *expected)
+          operations[collection].collect {|op| op.name }.should =~ expected
+        end
+
+        client.should_have_operations(:buckets, :index, :show, :destroy, :create)
+        client.should_have_operations(:hardware_profiles, :index, :show)
+        client.should_have_operations(:images, :index, :show)
+        client.should_have_operations(:instances, :index, :show, :destroy, :start, :stop,
:create, :reboot)
+        client.should_have_operations(:instance_states, :index)
+        client.should_have_operations(:keys, :index, :show, :destroy, :create)
+        client.should_have_operations(:realms, :index, :show)
+        client.should_have_operations(:storage_snapshots, :index, :show, :destroy, :create)
+        client.should_have_operations(:storage_volumes, :index, :show, :destroy, :attach,
:detach, :create)
+      end
+    end
+
+    it "should load the features for each operation" do
+      DeltaCloud.new( "name", "password", API_URL ) do |client|
+        def client.should_have_operation_features(collection, operation, *expected)
+          operations[collection].find {|op| op.name == operation}.features.should =~ expected
+        end
+
+        client.should_have_operation_features(:instances, :create, :hardware_profiles, :user_name,
:authentication_key)
+        # hmm, the mock driver exposes very few features
+      end
+    end
+  end
+
+  describe 'declare_entry_points_methods' do
+    before(:all) do
+      @v1_xml = File.read(SPEC_DIR + '/fixtures/api/v1_api.xml')
+      @v2_xml = File.read(SPEC_DIR + '/fixtures/api/v2_api.xml')
+    end
+
+    context "when no operations are defined" do
+      before(:each) do
+        response = mock('response', :body => @v1_xml, :code => 200)
+        RestClient.stub(:send).and_yield(response, nil, nil)
+        @client = DeltaCloud.new( "name", "password", API_URL )
+      end
+
+      it "should define the collection method" do
+        @client.should respond_to(:instances)
+      end
+
+      it "should define the member method" do
+        @client.should respond_to(:instance)
+      end
+
+      it "should define the fetch method" do
+        @client.should respond_to(:fetch_instance)
+      end
+    end
+
+    context "when some operations are defined" do
+      it "should define the collection method when the index operation is defined" do
+        response = mock('response', :body => @v2_xml, :code => 200)
+        RestClient.stub(:send).and_yield(response, nil, nil)
+        DeltaCloud.new( "name", "password", API_URL ).should respond_to(:instances)
+      end
+
+
+      it "should not define the collection method when the index operation is not defined"
do
+        response = mock('response', :body => @v2_xml.gsub('index', 'not_index'), :code
=> 200)
+        RestClient.stub(:send).and_yield(response, nil, nil)
+        DeltaCloud.new( "name", "password", API_URL ).should_not respond_to(:instances)
+      end
+
+      it "should define the member and fetch methods when the index operation is defined"
do
+        response = mock('response', :body => @v2_xml, :code => 200)
+        RestClient.stub(:send).and_yield(response, nil, nil)
+        client = DeltaCloud.new( "name", "password", API_URL )
+        client.should respond_to(:instance)
+        client.should respond_to(:fetch_instance)
+      end
+
+
+      it "should not define the member and fetch methods when the index operation is not
defined" do
+        response = mock('response', :body => @v2_xml.gsub('show', 'not_show'), :code =>
200)
+        RestClient.stub(:send).and_yield(response, nil, nil)
+        client = DeltaCloud.new( "name", "password", API_URL )
+        client.should_not respond_to(:instance)
+        client.should_not respond_to(:fetch_instance)
+      end
+    end
+  end
+
+  describe "create methods" do
+    before(:each) do
+      @v2_xml = File.read(SPEC_DIR + '/fixtures/api/v2_api.xml')
+      response = mock('response', :body => @v2_xml, :code => 200)
+      RestClient.stub(:send).and_yield(response, nil, nil)
+      @client = DeltaCloud.new( "name", "password", API_URL )
+    end
+
+    it "should not define a create method for a collection that does not support create"
do
+      lambda{ @client.create_hardware_profile({ }) }.should raise_error(NoMethodError)
+    end
+
+    it "should define a create method for a collection that does support create" do
+      lambda{ @client.create_key({ }) }.should_not raise_error(NoMethodError)
+    end
+  end
+
+  # TODO: move this out of initialization_spec
+  describe "on driver switch" do
+    before(:each) do
+      @v2_xml = File.read(SPEC_DIR + '/fixtures/api/v2_api.xml')
+      @response = mock('response', :body => @v2_xml, :code => 200)
+      RestClient.stub(:send).and_yield(@response, nil, nil)
+      @client = DeltaCloud.new( "name", "password", API_URL )
+    end
+
+    it "should reset the operations" do
+      @client.allow_operation?(:instances, :index).should be_true
+      @v2_xml.gsub!('index', 'not_index')
+      @client.use_driver(:xxx)
+      @client.allow_operation?(:instances, :index).should_not be_true
+    end
+
+    it "should reset the defined methods" do
+      @client.should respond_to(:instances)
+      @client.should respond_to(:instance)
+      @client.should respond_to(:fetch_instance)
+      lambda{ @client.create_instance({ }) }.should_not raise_error(NoMethodError)
+
+      @v2_xml.gsub!('index', 'not_index')
+      @v2_xml.gsub!('show', 'not_show')
+      @v2_xml.gsub!('create', 'not_create')
+      @client.use_driver(:xxx)
+
+      @client.should_not respond_to(:instances)
+      @client.should_not respond_to(:instance)
+      @client.should_not respond_to(:fetch_instance)
+      lambda{ @client.create_instance({ }) }.should raise_error(NoMethodError)
+    end
+  end
+
+  # TODO: move this out of initialization_spec
+  describe "allow_operation?" do
+    before(:each) do
+      @response = mock('response', :code => 200)
+      RestClient.stub(:send).and_yield(@response, nil, nil)
+    end
+
+    context "when no operations are specified in the api response" do
+      before(:each) do
+        @v1_xml = File.read(SPEC_DIR + '/fixtures/api/v1_api.xml')
+        @response.stub(:body).and_return(@v1_xml)
+        @client = DeltaCloud.new( "name", "password", API_URL )
+      end
+
+      it "should allow any operation if no operations are specified in the api response"
do
+        @client.allow_operation?(:instances, :not_a_real_operation).should be_true
+      end
+
+      it "should raise if given a nonexistent collection" do
+        lambda { @client.allow_operation?(:dollars, :index) }.should raise_error(ArgumentError)
+      end
+    end
+
+    context 'when operations are specified in the api reponse' do
+      before(:each) do
+        @v2_xml = File.read(SPEC_DIR + '/fixtures/api/v2_api.xml')
+        @response.stub(:body).and_return(@v2_xml)
+        @client = DeltaCloud.new( "name", "password", API_URL )
+      end
+
+      it "should allow an operation specified in the api response" do
+        @client.allow_operation?(:instances, :index).should be_true
+      end
+
+      it "should not allow an operation not specified in the api response" do
+        @response.stub(:body).and_return(@v2_xml)
+        @client = DeltaCloud.new( "name", "password", API_URL )
+        @client.allow_operation?(:instances, :not_a_real_operation).should_not be_true
+      end
+
+      it "should raise if given a nonexistent collection" do
+        lambda { @client.allow_operation?(:dollars, :index) }.should raise_error(ArgumentError)
+      end
+    end
+  end
+
+  describe "features_for_operation" do
+    before(:each) do
+      @response = mock('response', :code => 200)
+      RestClient.stub(:send).and_yield(@response, nil, nil)
+    end
+
+    context "when no operations are specified in the api response" do
+      before(:each) do
+        @v1_xml = File.read(SPEC_DIR + '/fixtures/api/v1_api.xml')
+        @response.stub(:body).and_return(@v1_xml)
+        @client = DeltaCloud.new( "name", "password", API_URL )
+      end
+
+      it "should raise since when features are requested for any operation" do
+        lambda { @client.features_for_operation(:instances, :index) }.should raise_error(ArgumentError)
+      end
+    end
+    
+    context 'when operations are specified in the api reponse' do
+      before(:each) do
+        @v2_xml = File.read(SPEC_DIR + '/fixtures/api/v2_api.xml')
+        @response.stub(:body).and_return(@v2_xml)
+        @client = DeltaCloud.new( "name", "password", API_URL )
+      end
+
+      it "should raise if given a nonexistent collection" do
+        lambda { @client.features_for_operation(:dollars, :index) }.should raise_error(ArgumentError)
+      end
+
+      it "should raise if given a nonexistent operation" do
+        lambda { @client.features_for_operation(:instances, :not_an_operation) }.should raise_error(ArgumentError)
+      end
+
+      it "shoud return [] for an operation with no features" do
+        @client.features_for_operation(:instances, :index).should == []
+      end
+
+      it "shoud return the features for an operation with  features" do
+        @client.features_for_operation(:instances, :create).should =~ [:hardware_profiles,
:user_name, :authentication_key]
+      end
+    end
+  end
 end
diff --git a/client/specs/spec_helper.rb b/client/specs/spec_helper.rb
index b4cb95c..0ee95a0 100644
--- a/client/specs/spec_helper.rb
+++ b/client/specs/spec_helper.rb
@@ -37,17 +37,19 @@ API_URL_REDIRECT = "http://#{API_HOST}:#{API_PORT}"
 API_NAME     = 'mockuser'
 API_PASSWORD = 'mockpassword'
 
-$: << File.dirname( __FILE__ ) + '/../lib'
+SPEC_DIR = File.expand_path(File.dirname(__FILE__))
+
+$: << SPEC_DIR + '/../lib'
 require 'deltacloud'
 
 def clean_fixtures
-  FileUtils.rm_rf( File.dirname( __FILE__ ) + '/data' )
+  FileUtils.rm_rf( SPEC_DIR + '/data' )
 end
 
 def reload_fixtures
   clean_fixtures
-  FileUtils.cp_r( File.dirname( __FILE__) + '/fixtures', File.dirname( __FILE__ ) + '/data'
)
+  FileUtils.cp_r( SPEC_DIR + '/fixtures', SPEC_DIR + '/data' )
 end
 
-$: << File.dirname( __FILE__ )
+$: << SPEC_DIR
 require 'shared/resources'
diff --git a/server/lib/sinatra/rabbit.rb b/server/lib/sinatra/rabbit.rb
index e843366..8cf41e1 100644
--- a/server/lib/sinatra/rabbit.rb
+++ b/server/lib/sinatra/rabbit.rb
@@ -41,6 +41,10 @@ module Sinatra
         STANDARD.keys.include?(name)
       end
 
+      def scope
+        @member ? :member : :collection
+      end
+      
       def description(text="")
         return @description if text.blank?
         @description = text
@@ -234,6 +238,10 @@ module Sinatra
         m << [ coll.name, url ]
       end
     end
+
+    def operations_for_collection(collection)
+      collections[collection].operations.values
+    end
   end
 
   register Rabbit
diff --git a/server/views/api/show.xml.haml b/server/views/api/show.xml.haml
index f68fd79..59fb3e6 100644
--- a/server/views/api/show.xml.haml
+++ b/server/views/api/show.xml.haml
@@ -3,3 +3,7 @@
     %link{ :rel=>entry_point[0], :href=>entry_point[1] }
       - for feature in driver.features(entry_point[0])
         %feature{ :name=>feature.name }
+      - for operation in operations_for_collection(entry_point[0]) 
+        %operation{ :name => operation.name, :method => operation.method, :scope =>
operation.scope }
+          - for feature in driver.features_for_operation(entry_point[0], operation.name)
+            %param{ :feature => feature.name }
-- 
1.7.3.2


Mime
View raw message