See comments, below: ----- [email protected] wrote:
> From: Michal Fojtik <[email protected]> > > --- > 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 In the method_missing() method there's no need for 'else' statement, because you're calling return before it. Also I think you can write just 'return' instead of 'return nil'. > + > + private > + > + def search_for_method(name) > + @objects.select { |o| o[:method_name] == "#{name}" }.first > + end Doing 'select' and than 'first' is better to do with method 'detect' or 'find' from Enumerable. It's faster. > + > + 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 Here with 'select' and 'collect' you're iterating 2x. You could use just 'inject' to do one pass. > + > + 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 > > [email protected]_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 << "# > #[email protected](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 << "# > #[email protected](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 I still have to test it locally after that I will send ACK if nobody else does first. -- Ladislav
