You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@deltacloud.apache.org by mf...@redhat.com on 2010/10/15 17:42:35 UTC

[PATCH core 2/4] Fixed xml_to_class method in client

From: Michal Fojtik <mf...@redhat.com>

---
 client/lib/base_object.rb              |  281 +++++++++++++++++++++
 client/lib/deltacloud.rb               |  434 ++++++++------------------------
 client/lib/documentation.rb            |  136 ++++-------
 client/lib/hwp_properties.rb           |   64 +++++
 client/lib/instance_state.rb           |   29 +++
 client/lib/string.rb                   |   53 ++++
 client/specs/hardware_profiles_spec.rb |    2 +-
 client/specs/instances_spec.rb         |    4 +-
 8 files changed, 589 insertions(+), 414 deletions(-)
 create mode 100644 client/lib/base_object.rb
 create mode 100644 client/lib/hwp_properties.rb
 create mode 100644 client/lib/instance_state.rb
 create mode 100644 client/lib/string.rb

diff --git a/client/lib/base_object.rb b/client/lib/base_object.rb
new file mode 100644
index 0000000..b269bb8
--- /dev/null
+++ b/client/lib/base_object.rb
@@ -0,0 +1,281 @@
+#
+# Copyright (C) 2010  Red Hat, Inc.
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+require 'lib/string'
+
+module DeltaCloud
+
+    class BaseObjectParamError < Exception; end
+    class NoHandlerForMethod < Exception; end
+
+    # BaseObject model basically provide the basic operation around
+    # REST model, like defining a links between different objects,
+    # element with text values, or collection of these elements
+    class BaseObject
+      attr_reader :id, :url, :client, :base_name
+      attr_reader :objects
+
+      alias :uri :url
+
+      # For initializing new object you require to set
+      # id, url, client and name attribute.
+      def initialize(opts={}, &block)
+        @id, @url, @client, @base_name = opts[:id], opts[:url], opts[:client], opts[:name]
+        @objects = []
+        raise BaseObjectParamError if @id.nil? or @url.nil? or @client.nil? or @base_name.nil?
+        yield self if block_given?
+      end
+
+      # This method add link to another object in REST model
+      # XML syntax: <link rel="destroy" href="http://localhost/api/resource" method="post"/>
+      def add_link!(object_name, id)
+        @objects << {
+          :type => :link,
+          :method_name => object_name.sanitize,
+          :id => id
+        }
+        @objects << {
+          :type => :text,
+          :method_name => "#{object_name.sanitize}_id",
+          :value => id
+        }
+      end
+
+      # Method add property for hardware profile
+      def add_hwp_property!(name, property, type)
+        hwp_property=case type
+          when :float then DeltaCloud::HWP::FloatProperty.new(property, name)
+          when :integer then DeltaCloud::HWP::Property.new(property, name)
+        end
+        @objects << {
+          :type => :property,
+          :method_name => name.sanitize,
+          :property => hwp_property
+        }
+      end
+
+      # This method define text object in REST model
+      # XML syntax: <name>Instance 1</name>
+      def add_text!(object_name, value)
+        @objects << {
+          :type => :text,
+          :method_name => object_name.sanitize,
+          :value => value
+        }
+      end
+
+      # This method define collection of text elements inside REST model
+      # XML syntax: <addresses>
+      #               <address>127.0.0.1</address>
+      #               <address>127.0.0.2</address>
+      #             </addresses>
+      def add_collection!(collection_name, values=[])
+        @objects << {
+          :type => :collection,
+          :method_name => collection_name.sanitize,
+          :values => values
+        }
+      end
+
+      # Basic method hander. This define a way how value from property
+      # will be returned
+      def method_handler(m, args=[])
+        case m[:type]
+          when :link then return @client.send(m[:method_name].singularize, m[:id])
+          when :text then return m[:value]
+          when :property then return m[:property]
+          when :collection then return m[:values]
+          else raise NoHandlerForMethod
+        end
+      end
+
+      def method_missing(method_name, *args)
+        # First of all search throught array for method name
+        m = search_for_method(method_name)
+        if m.nil?
+          warn "[WARNING] Method unsupported by API: '#{self.class}.#{method_name}(#{args.inspect})'"
+          return nil
+        else
+          # Call appropriate handler for method
+          method_handler(m, args)
+        end
+      end
+
+      private
+
+      def search_for_method(name)
+        @objects.select { |o| o[:method_name] == "#{name}" }.first
+      end
+
+    end
+
+    class ActionObject < BaseObject
+
+      def initialize(opts={}, &block)
+        super(opts)
+        @action_urls = opts[:action_urls] || []
+        @actions = []
+      end
+
+      # This trigger is called right after action. 
+      # This method does nothing inside ActionObject
+      # but it can be redifined and used in meta-programming
+      def action_trigger(action)
+      end
+
+      def add_action_link!(id, link)
+        m = {
+          :type => :action_link,
+          :method_name => "#{link['rel'].sanitize}!",
+          :id => id,
+          :href => link['href'],
+          :rel => link['rel'].sanitize,
+          :method => link['method'].sanitize
+        }
+        @objects << m
+        @actions << [m[:rel], m[:href]]
+        @action_urls << m[:href]
+      end
+
+      def actions
+        @objects.select {|o| o[:type].eql?(:action_link) }.collect { |o| [o[:rel], o[:href]] }
+      end
+
+      def action_urls
+        actions.collect { |a| a.last }
+      end
+
+      alias :base_method_handler :method_handler
+
+      # First call BaseObject method handler,
+      # then, if not method found try ActionObject handler
+      def method_handler(m, args=[])
+        begin
+          base_method_handler(m, args)
+        rescue NoHandlerForMethod
+          case m[:type]
+            when :action_link then do_action(m)
+            else raise NoHandlerForMethod
+          end
+        end
+      end
+
+      private
+
+      def do_action(m)
+        @client.request(:"#{m[:method]}", m[:href], {}, {})
+        action_trigger(m[:rel])
+      end
+
+    end
+
+    class StateFullObject < ActionObject
+      attr_reader :state
+
+      def initialize(opts={}, &block)
+        super(opts)
+        @state = opts[:initial_state] || ''
+        add_default_states!
+      end
+
+      def add_default_states!
+        @objects << {
+          :method_name => 'stopped?',
+          :type => :state,
+          :state => 'STOPPED'
+        }
+        @objects << {
+          :method_name => 'running?',
+          :type => :state,
+          :state => 'RUNNING'
+        }
+        @objects << {
+          :method_name => 'pending?',
+          :type => :state,
+          :state => 'PENDING'
+        }
+        @objects << {
+          :method_name => 'shutting_down?',
+          :type => :state,
+          :state => 'SHUTTING_DOWN'
+        }
+      end
+
+      def action_trigger(action)
+        # Refresh object state after action
+        @new_state_object = @client.send(self.base_name, self.id)
+        @state = @new_state_object.state
+        self.update_actions!
+      end
+
+      alias :action_method_handler :method_handler
+
+      def method_handler(m, args=[])
+        begin
+          action_method_handler(m, args)
+        rescue NoHandlerForMethod
+          case m[:type]
+            when :state then evaluate_state(m[:state], @state)
+            else raise NoHandlerForMethod
+          end
+        end
+      end
+
+#      private
+
+      def evaluate_state(method_state, current_state)
+        method_state.eql?(current_state)
+      end
+
+      def action_objects
+        @objects.select { |o| o[:type] == :action_link }
+      end
+
+      def update_actions!
+        new_actions = @new_state_object.action_objects
+        @objects.reject! { |o| o[:type] == :action_link }
+        @objects = (@objects + new_actions)
+      end
+
+    end
+
+    def self.add_class(name, parent=:base)
+      parent_class = case parent
+        when :base then BaseObject
+        when :action then ActionObject
+        when :state then StateFullObject
+      end
+      begin
+        return API.const_get(name.classify)
+      rescue NameError
+        API.module_eval("class #{name.classify} < #{parent_class.to_s}; end")
+        new_class = API.const_get(name.classify)
+        @defined_classes ||= []
+        @defined_classes << new_class
+        new_class
+      end
+    end
+
+    def self.guess_model_type(response)
+      response = Nokogiri::XML(response.to_s)
+      return :action if ((response/'//actions').length == 1) and ((response/'//state').length == 0)
+      return :state if ((response/'//actions').length == 1) and ((response/'//state').length == 1)
+      return :base
+    end
+
+end
diff --git a/client/lib/deltacloud.rb b/client/lib/deltacloud.rb
index e44556f..5c224e5 100644
--- a/client/lib/deltacloud.rb
+++ b/client/lib/deltacloud.rb
@@ -20,6 +20,10 @@ require 'nokogiri'
 require 'rest_client'
 require 'base64'
 require 'logger'
+require 'lib/hwp_properties'
+require 'lib/instance_state'
+require 'lib/documentation'
+require 'lib/base_object'
 
 module DeltaCloud
 
@@ -56,27 +60,11 @@ module DeltaCloud
     API.new(nil, nil, url).driver_name
   end
 
-  def self.define_class(name)
-    @defined_classes ||= []
-    if @defined_classes.include?(name)
-      self.module_eval("API::#{name}")
-    else
-      @defined_classes << name unless @defined_classes.include?(name)
-      API.const_set(name, Class.new)
-    end
-  end
-
-  def self.classes
-    @defined_classes || []
-  end
-
   class API
-    attr_accessor :logger
     attr_reader   :api_uri, :driver_name, :api_version, :features, :entry_points
 
     def initialize(user_name, password, api_url, opts={}, &block)
       opts[:version] = true
-      @logger = opts[:verbose] ? Logger.new(STDERR) : []
       @username, @password = user_name, password
       @api_uri = URI.parse(api_url)
       @features, @entry_points = {}, {}
@@ -101,147 +89,91 @@ 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)
-      logger = @logger
+      
       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|
-              # Define a new class based on model name
-              c = DeltaCloud.define_class("#{model.to_s.classify}")
-              # Create collection from index operation
-              base_object_collection(c, model, response)
+              base_object_collection(model, response)
             end
           end
-          logger << "[API] Added method #{model}\n"
+          
           define_method :"#{model.to_s.singularize}" do |*args|
             request(:get, "#{entry_points[model]}/#{args[0]}") do |response|
-              # Define a new class based on model name
-              c = DeltaCloud.define_class("#{model.to_s.classify}")
-              # Build class for returned object
-              base_object(c, model, response)
+              base_object(model, response)
             end
           end
-          logger << "[API] Added method #{model.to_s.singularize}\n"
+          
           define_method :"fetch_#{model.to_s.singularize}" do |url|
             id = url.grep(/\/#{model}\/(.*)$/)
             self.send(model.to_s.singularize.to_sym, $1)
           end
+
         end
       end
     end
 
-    def base_object_collection(c, model, response)
-      collection = []
-      Nokogiri::XML(response).xpath("#{model}/#{model.to_s.singularize}").each do |item|
-        c.instance_eval do
-          attr_accessor :id
-          attr_accessor :uri
-        end
-        collection << xml_to_class(c, item)
+    def base_object_collection(model, response)
+      Nokogiri::XML(response).xpath("#{model}/#{model.to_s.singularize}").collect do |item|
+        base_object(model, item.to_s)
       end
-      return collection
     end
 
     # Add default attributes [id and href] to class
-    def base_object(c, model, response)
-      obj = nil
-      Nokogiri::XML(response).xpath("#{model.to_s.singularize}").each do |item|
-        c.instance_eval do
-          attr_accessor :id
-          attr_accessor :uri
-
-
-        end
-        obj = xml_to_class(c, item)
-      end
-      return obj
+    def base_object(model, response)
+      c = DeltaCloud.add_class("#{model}", DeltaCloud::guess_model_type(response))
+      xml_to_class(c, Nokogiri::XML(response).xpath("#{model.to_s.singularize}").first)
     end
 
     # Convert XML response to defined Ruby Class
-    def xml_to_class(c, item)
-      obj = c.new
-      # Set default attributes
-      obj.id = item['id']
-      api = self
-      c.instance_eval do
-        define_method :method_missing do |method|
-            warn "[WARNING] Method '#{method}' is not available for this resource (#{c.name})."
-            return nil
-        end
-        define_method :client do
-          api
+    def xml_to_class(base_object, item)
+      
+      return nil unless item
+
+      params = {
+          :id => item['id'],
+          :url => item['href'],
+          :name => item.name,
+          :client => self
+      }
+      params.merge!({ :initial_state => (item/'state').text.sanitize }) if (item/'state').length > 0
+
+      obj = base_object.new(params)
+
+      # Traverse across XML document and deal with elements
+      item.xpath('./*').each do |attribute|
+
+        # Do a link for elements which are links to other REST models
+        if self.entry_points.keys.include?(:"#{attribute.name}s")
+          obj.add_link!(attribute.name, attribute['id']) && next
         end
-      end
-      obj.uri = item['href']
-      logger = @logger
-      logger << "[DC] Creating class #{obj.class.name}\n"
-      obj.instance_eval do
-        # Declare methods for all attributes in object
-        item.xpath('./*').each do |attribute|
-          # If attribute is a link to another object then
-          # create a method which request this object from API
-          if api.entry_points.keys.include?(:"#{attribute.name}s")
-            c.instance_eval do
-              define_method :"#{attribute.name.sanitize}" do
-                client.send(:"#{attribute.name}", attribute['id'] )
-              end
-              logger << "[DC] Added #{attribute.name} to class #{obj.class.name}\n"
-            end
+
+        # Do a HWP property for hardware profile properties
+        if attribute.name == 'property'
+          if attribute['value'] =~ /^(\d+)$/
+            obj.add_hwp_property!(attribute['name'], attribute, :float) && next
           else
-            # Define methods for other attributes
-            c.instance_eval do
-              case attribute.name
-                # When response cointains 'link' block, declare
-                # methods to call links inside. This is used for instance
-                # to dynamicaly create .stop!, .start! methods
-                when "actions" then
-                  actions = []
-                  attribute.xpath('link').each do |link|
-                    actions << [link['rel'], link[:href]]
-                    define_method :"#{link['rel'].sanitize}!" do
-                      client.request(:"#{link['method']}", link['href'], {}, {})
-                      @current_state = client.send(:"#{item.name}", item['id']).state
-                      obj.instance_eval do |o|
-                        def state
-                          @current_state
-                        end
-                      end
-                    end
-                  end
-                  define_method :actions do
-                    actions.collect { |a| a.first }
-                  end
-                  define_method :actions_urls do
-                    urls = {}
-                    actions.each { |a| urls[a.first] = a.last }
-                    urls
-                  end
-                # Property attribute is handled differently
-                when "property" then
-                  attr_accessor :"#{attribute['name'].sanitize}"
-                  if attribute['value'] =~ /^(\d+)$/
-                    obj.send(:"#{attribute['name'].sanitize}=",
-                      DeltaCloud::HWP::FloatProperty.new(attribute, attribute['name']))
-                  else
-                    obj.send(:"#{attribute['name'].sanitize}=",
-                      DeltaCloud::HWP::Property.new(attribute, attribute['name']))
-                  end
-                # Public and private addresses are returned as Array
-                when "public_addresses", "private_addresses" then
-                  attr_accessor :"#{attribute.name.sanitize}"
-                  obj.send(:"#{attribute.name.sanitize}=",
-                    attribute.xpath('address').collect { |address| address.text })
-                # Value for other attributes are just returned using
-                # method with same name as attribute (eg. .owner_id, .state)
-                else
-                  attr_accessor :"#{attribute.name.sanitize}"
-                  obj.send(:"#{attribute.name.sanitize}=", attribute.text.convert)
-                  logger << "[DC] Added method #{attribute.name}[#{attribute.text}] to #{obj.class.name}\n"
-              end
-            end
+            obj.add_hwp_property!(attribute['name'], attribute, :integer) && next
           end
         end
+
+        # If there are actions, add they to ActionObject/StateFullObject
+        if attribute.name == 'actions'
+          (attribute/'link').each do |link|
+            obj.add_action_link!(item['id'], link)
+          end && next
+        end
+
+        # Deal with collections like public-addresses, private-addresses
+        if (attribute/'./*').length > 0
+          obj.add_collection!(attribute.name, (attribute/'*').collect { |value| value.text }) && next
+        end
+        
+        # Anything else is treaten as text object
+        obj.add_text!(attribute.name, attribute.text.convert)
       end
+
       return obj
     end
 
@@ -252,73 +184,54 @@ module DeltaCloud
         api_xml = Nokogiri::XML(response)
         @driver_name = api_xml.xpath('/api').first['driver']
         @api_version = api_xml.xpath('/api').first['version']
-        logger << "[API] Version #{@api_version}\n"
-        logger << "[API] Driver #{@driver_name}\n"
+        
         api_xml.css("api > link").each do |entry_point|
           rel, href = entry_point['rel'].to_sym, entry_point['href']
           @entry_points.store(rel, href)
-          logger << "[API] Entry point '#{rel}' added\n"
+          
           entry_point.css("feature").each do |feature|
             @features[rel] ||= []
             @features[rel] << feature['name'].to_sym
-            logger << "[API] Feature #{feature['name']} added to #{rel}\n"
+            
           end
         end
       end
       declare_entry_points_methods(@entry_points)
     end
 
-    def create_key(opts={}, &block)
-      params = { :name => opts[:name] }
-      key = nil
-      request(:post, entry_points[:keys], {}, params) do |response|
-        c = DeltaCloud.define_class("Key")
-        key = base_object(c, :key, response)
-        yield key if block_given?
-      end
-      return key
-    end
-
-    # Create a new instance, using image +image_id+. Possible optiosn are
+    # Generate create_* methods dynamically
     #
-    #   name  - a user-defined name for the instance
-    #   realm - a specific realm for placement of the instance
-    #   hardware_profile - either a string giving the name of the
-    #                      hardware profile or a hash. The hash must have an
-    #                      entry +id+, giving the id of the hardware profile,
-    #                      and may contain additional names of properties,
-    #                      e.g. 'storage', to override entries in the
-    #                      hardware profile
-    def create_instance(image_id, opts={}, &block)
-      name = opts[:name]
-      realm_id = opts[:realm]
-      user_data = opts[:user_data]
-      key_name = opts[:key_name]
-
-      params = {}
-      ( params[:realm_id] = realm_id ) if realm_id
-      ( params[:name] = name ) if name
-      ( params[:user_data] = user_data ) if user_data
-      ( params[:keyname] = key_name ) if key_name
-
-      if opts[:hardware_profile].is_a?(String)
-        params[:hwp_id] = opts[:hardware_profile]
-      elsif opts[:hardware_profile].is_a?(Hash)
-        opts[:hardware_profile].each do |k,v|
-          params[:"hwp_#{k}"] = v
+    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
-      end
 
-      params[:image_id] = image_id
-      instance = nil
+        params[:image_id] ||= params[:image_id] || args[0] if args[0].class!=Hash
 
-      request(:post, entry_points[:instances], {}, params) do |response|
-        c = DeltaCloud.define_class("Instance")
-        instance = base_object(c, :instance, response)
-        yield instance if block_given?
-      end
+        obj = nil
 
-      return instance
+        request(:post, entry_points[:"#{$1}s"], {}, params) do |response|
+          obj = base_object(:"#{$1}", response)
+          yield obj if block_given?
+        end
+        return obj
+      end
+      raise NoMethodError
     end
 
     # Basic request method
@@ -333,9 +246,10 @@ module DeltaCloud
       if conf[:query_args] != {}
         conf[:path] += '?' + URI.escape(conf[:query_args].collect{ |key, value| "#{key}=#{value}" }.join('&')).to_s
       end
-      logger << "[#{conf[:method].to_s.upcase}] #{conf[:path]}\n"
+      
       if conf[:method].eql?(:post)
         RestClient.send(:post, conf[:path], conf[:form_data], default_headers) do |response, request, block|
+          handle_backend_error(response) if response.code.eql?(500)
           if response.respond_to?('body')
             yield response.body if block_given?
           else
@@ -344,6 +258,7 @@ module DeltaCloud
         end
       else
         RestClient.send(conf[:method], conf[:path], default_headers) do |response, request, block|
+          handle_backend_error(response) if response.code.eql?(500)
           if conf[:method].eql?(:get) and [301, 302, 307].include? response.code
             response.follow_redirection(request) do |response, request, block|
               if response.respond_to?('body')
@@ -363,6 +278,21 @@ module DeltaCloud
       end
     end
 
+    # Re-raise backend errors as on exception in client with message from
+    # backend
+    class BackendError < Exception
+      def initialize(opts={})
+        @message = opts[:message]
+      end
+      def message
+        @message
+      end
+    end
+
+    def handle_backend_error(response)
+      raise BackendError.new(:message => (Nokogiri::XML(response)/'error/message').text)
+    end
+
     # Check if specified collection have wanted feature
     def feature?(collection, name)
       @features.has_key?(collection) && @features[collection].include?(name)
@@ -396,6 +326,7 @@ module DeltaCloud
       true if @entry_points!={}
     end
 
+    # This method will retrieve API documentation for given collection
     def documentation(collection, operation=nil)
       data = {}
       request(:get, "/docs/#{collection}") do |body|
@@ -436,151 +367,4 @@ module DeltaCloud
 
   end
 
-  class Documentation
-
-    attr_reader :api, :description, :params, :collection_operations
-    attr_reader :collection, :operation
-
-    def initialize(api, opts={})
-      @description, @api = opts[:description], api
-      @params = parse_parameters(opts[:params]) if opts[:params]
-      @collection_operations = opts[:operations] if opts[:operations]
-      @collection = opts[:collection]
-      @operation = opts[:operation]
-      self
-    end
-
-    def operations
-      @collection_operations.collect { |o| api.documentation(@collection, o) }
-    end
-
-    class OperationParameter
-      attr_reader :name
-      attr_reader :type
-      attr_reader :required
-      attr_reader :description
-
-      def initialize(data)
-        @name, @type, @required, @description = data[:name], data[:type], data[:required], data[:description]
-      end
-
-      def to_comment
-        "   # @param [#{@type}, #{@name}] #{@description}"
-      end
-
-    end
-
-    private
-
-    def parse_parameters(params)
-      params.collect { |p| OperationParameter.new(p) }
-    end
-
-  end
-
-  module InstanceState
-
-    class State
-      attr_reader :name
-      attr_reader :transitions
-
-      def initialize(name)
-        @name, @transitions = name, []
-      end
-    end
-
-    class Transition
-      attr_reader :to
-      attr_reader :action
-
-      def initialize(to, action)
-        @to = to
-        @action = action
-      end
-
-      def auto?
-        @action.nil?
-      end
-    end
-  end
-
-  module HWP
-
-   class Property
-      attr_reader :name, :unit, :value, :kind
-
-      def initialize(xml, name)
-        @name, @kind, @value, @unit = xml['name'], xml['kind'].to_sym, xml['value'], xml['unit']
-        declare_ranges(xml)
-        self
-      end
-
-      def present?
-        ! @value.nil?
-      end
-
-      private
-
-      def declare_ranges(xml)
-        case xml['kind']
-          when 'range' then
-            self.class.instance_eval do
-              attr_reader :range
-            end
-            @range = { :from => xml.xpath('range').first['first'], :to => xml.xpath('range').first['last'] }
-          when 'enum' then
-            self.class.instance_eval do
-              attr_reader :options
-            end
-            @options = xml.xpath('enum/entry').collect { |e| e['value'] }
-        end
-      end
-
-    end
-
-    # FloatProperty is like Property but return value is Float instead of String.
-    class FloatProperty < Property
-      def initialize(xml, name)
-        super(xml, name)
-        @value = @value.to_f if @value
-      end
-    end
-  end
-
-end
-
-class String
-
-  unless method_defined?(:classify)
-    # Create a class name from string
-    def classify
-      self.singularize.camelize
-    end
-  end
-
-  unless method_defined?(:camelize)
-    # Camelize converts strings to UpperCamelCase
-    def camelize
-      self.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
-    end
-  end
-
-  unless method_defined?(:singularize)
-    # Strip 's' character from end of string
-    def singularize
-      self.gsub(/s$/, '')
-    end
-  end
-
-  # Convert string to float if string value seems like Float
-  def convert
-    return self.to_f if self.strip =~ /^([\d\.]+$)/
-    self
-  end
-
-  # Simply converts whitespaces and - symbols to '_' which is safe for Ruby
-  def sanitize
-    self.gsub(/(\W+)/, '_')
-  end
-
 end
diff --git a/client/lib/documentation.rb b/client/lib/documentation.rb
index 6fb5779..c6255af 100644
--- a/client/lib/documentation.rb
+++ b/client/lib/documentation.rb
@@ -1,98 +1,62 @@
-require 'lib/deltacloud'
+#
+# Copyright (C) 2010  Red Hat, Inc.
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+module DeltaCloud
+  class Documentation
+
+    attr_reader :api, :description, :params, :collection_operations
+    attr_reader :collection, :operation
+
+    def initialize(api, opts={})
+      @description, @api = opts[:description], api
+      @params = parse_parameters(opts[:params]) if opts[:params]
+      @collection_operations = opts[:operations] if opts[:operations]
+      @collection = opts[:collection]
+      @operation = opts[:operation]
+      self
+    end
 
-skip_methods = [ "id=", "uri=" ]
+    def operations
+      @collection_operations.collect { |o| api.documentation(@collection, o) }
+    end
 
-begin
-  @dc=DeltaCloud.new('mockuser', 'mockpassword', 'http://localhost:3001/api')
-rescue
-  puts "Please make sure that Deltacloud API is running with Mock driver"
-  exit(1)
-end
+    class OperationParameter
+      attr_reader :name
+      attr_reader :type
+      attr_reader :required
+      attr_reader :description
 
-@dc.entry_points.keys.each do |ep|
-  @dc.send(ep)
-end
+      def initialize(data)
+        @name, @type, @required, @description = data[:name], data[:type], data[:required], data[:description]
+      end
 
-class_list = DeltaCloud::classes.collect { |c| DeltaCloud::module_eval("::DeltaCloud::API::#{c}")}
+      def to_comment
+        "   # @param [#{@type}, #{@name}] #{@description}"
+      end
 
-def read_method_description(c, method)
-  if method =~ /es$/
-    "    # Read #{c.downcase} collection from Deltacloud API"
-  else
-    case method
-      when "uri" then
-        "    # Return URI to API for this object"
-      when "action_urls" then
-        "    # Return available actions API URL"
-      when "client" then
-        "    # Return instance of API client"
-      else
-        "    # Get #{method} attribute value from #{c.downcase}"
     end
-  end
-end
 
-def read_parameters(c, method)
-  out = []
-  if method =~ /es$/
-    out << "    # @param [String, #id] Filter by ID"
-  end
-  out.join("\n")
-end
+    private
 
-def read_return_value(c, method)
-  if method =~ /es$/
-    rt = "Array"
-  else
-    rt = "String"
-  end
-  "    # @return [String] Value of #{method}"
-end
-
-out = []
-
-class_list.each do |c|
-  class_name = "#{c}".gsub(/^DeltaCloud::/, '')
-  out << "module DeltaCloud"
-  out << "  class API"
-  @dc.entry_points.keys.each do |ep|
-    out << "# Return #{ep.to_s.classify} object with given id\n"
-    out << "# "
-    out << "# #{@dc.documentation(ep.to_s).description.split("\n").join("\n# ")}"
-    out << "# @return [#{ep.to_s.classify}]"
-    out << "def #{ep.to_s.gsub(/s$/, '')}"
-    out << "end"
-    out << "# Return collection of #{ep.to_s.classify} objects"
-    out << "# "
-    out << "# #{@dc.documentation(ep.to_s).description.split("\n").join("\n# ")}"
-    @dc.documentation(ep.to_s, 'index').params.each do |p|
-      out << p.to_comment
+    def parse_parameters(params)
+      params.collect { |p| OperationParameter.new(p) }
     end
-    out << "# @return [Array] [#{ep.to_s.classify}]"
-    out << "def #{ep}(opts={})"
-    out << "end"
-  end
-  out << "  end"
-  out << "  class #{class_name}"
-  c.instance_methods(false).each do |method|
-    next if skip_methods.include?(method)
-    params = read_parameters(class_name, method)
-    retval = read_return_value(class_name, method)
-    out << read_method_description(class_name, method)
-    out << params if params
-    out << retval if retval
-    out << "    def #{method}"
-    out << "      # This method was generated dynamically from API"
-    out << "    end\n"
+
   end
-  out << "  end"
-  out << "end"
-end
 
-FileUtils.rm_r('doc') rescue nil
-FileUtils.mkdir_p('doc')
-File.open('doc/deltacloud.rb', 'w') do |f|
-  f.puts(out.join("\n"))
 end
-system("yardoc -m markdown --readme README --title 'Deltacloud Client Library' 'lib/*.rb' 'doc/deltacloud.rb' --verbose")
-FileUtils.rm('doc/deltacloud.rb')
diff --git a/client/lib/hwp_properties.rb b/client/lib/hwp_properties.rb
new file mode 100644
index 0000000..86ccf00
--- /dev/null
+++ b/client/lib/hwp_properties.rb
@@ -0,0 +1,64 @@
+#
+# Copyright (C) 2009  Red Hat, Inc.
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+module DeltaCloud
+
+  module HWP
+
+   class Property
+      attr_reader :name, :unit, :value, :kind
+
+      def initialize(xml, name)
+        @name, @kind, @value, @unit = xml['name'], xml['kind'].to_sym, xml['value'], xml['unit']
+        declare_ranges(xml)
+        self
+      end
+
+      def present?
+        ! @value.nil?
+      end
+
+      private
+
+      def declare_ranges(xml)
+        case xml['kind']
+          when 'range' then
+            self.class.instance_eval do
+              attr_reader :range
+            end
+            @range = { :from => xml.xpath('range').first['first'], :to => xml.xpath('range').first['last'] }
+          when 'enum' then
+            self.class.instance_eval do
+              attr_reader :options
+            end
+            @options = xml.xpath('enum/entry').collect { |e| e['value'] }
+        end
+      end
+
+    end
+
+    # FloatProperty is like Property but return value is Float instead of String.
+    class FloatProperty < Property
+      def initialize(xml, name)
+        super(xml, name)
+        @value = @value.to_f if @value
+      end
+    end
+  end
+
+end
diff --git a/client/lib/instance_state.rb b/client/lib/instance_state.rb
new file mode 100644
index 0000000..ebc59c4
--- /dev/null
+++ b/client/lib/instance_state.rb
@@ -0,0 +1,29 @@
+module DeltaCloud
+  module InstanceState
+
+    class State
+      attr_reader :name
+      attr_reader :transitions
+
+      def initialize(name)
+        @name, @transitions = name, []
+      end
+    end
+
+    class Transition
+      attr_reader :to
+      attr_reader :action
+
+      def initialize(to, action)
+        @to = to
+        @action = action
+      end
+
+      def auto?
+        @action.nil?
+      end
+    end
+
+  end
+
+end
diff --git a/client/lib/string.rb b/client/lib/string.rb
new file mode 100644
index 0000000..72cc259
--- /dev/null
+++ b/client/lib/string.rb
@@ -0,0 +1,53 @@
+#
+# Copyright (C) 2010  Red Hat, Inc.
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+class String
+
+  unless method_defined?(:classify)
+    # Create a class name from string
+    def classify
+      self.singularize.camelize
+    end
+  end
+
+  unless method_defined?(:camelize)
+    # Camelize converts strings to UpperCamelCase
+    def camelize
+      self.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
+    end
+  end
+
+  unless method_defined?(:singularize)
+    # Strip 's' character from end of string
+    def singularize
+      self.gsub(/s$/, '')
+    end
+  end
+
+  # Convert string to float if string value seems like Float
+  def convert
+    return self.to_f if self.strip =~ /^([\d\.]+$)/
+    self
+  end
+
+  # Simply converts whitespaces and - symbols to '_' which is safe for Ruby
+  def sanitize
+    self.strip.gsub(/(\W+)/, '_')
+  end
+
+end
diff --git a/client/specs/hardware_profiles_spec.rb b/client/specs/hardware_profiles_spec.rb
index d11eb36..7957e99 100644
--- a/client/specs/hardware_profiles_spec.rb
+++ b/client/specs/hardware_profiles_spec.rb
@@ -64,7 +64,7 @@ describe "hardware_profiles" do
   it "should allow fetching different hardware_profiles" do
     client = DeltaCloud.new( API_NAME, API_PASSWORD, API_URL )
     hwp1 = client.hardware_profile( 'm1-small' )
-    hwp2 = client.hardware_profile( 'm1-xlarge' )
+    hwp2 = client.hardware_profile( 'm1-large' )
     hwp1.storage.value.should_not eql(hwp2.storage.value)
     hwp1.memory.value.should_not eql(hwp2.memory.value)
   end
diff --git a/client/specs/instances_spec.rb b/client/specs/instances_spec.rb
index c4995ae..bfe29bb 100644
--- a/client/specs/instances_spec.rb
+++ b/client/specs/instances_spec.rb
@@ -85,7 +85,7 @@ describe "instances" do
 
   it "should allow creation of new instances with reasonable defaults" do
     DeltaCloud.new( API_NAME, API_PASSWORD, API_URL ) do |client|
-      instance = client.create_instance( 'img1', :name=>'TestInstance' )
+      instance = client.create_instance( 'img1', :name=>'TestInstance', :hardware_profile => 'm1-large' )
       instance.should_not be_nil
       instance.uri.should match( %r{#{API_URL}/instances/inst[0-9]+} )
       instance.id.should match( /inst[0-9]+/ )
@@ -98,7 +98,7 @@ describe "instances" do
 
   it "should allow creation of new instances with specific realm" do
     DeltaCloud.new( API_NAME, API_PASSWORD, API_URL ) do |client|
-      instance = client.create_instance( 'img1', :realm=>'eu' )
+      instance = client.create_instance( 'img1', :realm=>'eu', :hardware_profile => 'm1-large' )
       instance.should_not be_nil
       instance.uri.should match( %r{#{API_URL}/instances/inst[0-9]+} )
       instance.id.should match( /inst[0-9]+/ )
-- 
1.7.2.3