From fff4a69ec5a9d8742859e6b6dfc7e3cc05f25033 Mon Sep 17 00:00:00 2001 From: Alberto Bellotti Date: Thu, 26 Jan 2017 17:27:24 -0500 Subject: [PATCH] Adding support for subcollections and related subresources - Dynamically driven by the "subcollections" exposed via OPTIONS /api/:collection - supports queries and actions Queries: miq.vms.find(166).tags.collect(&:name) miq.vms.find(166).tags.select(:categorization).collect(&:categorization) Subcollection Actions: miq.vms.find(166).tags.assign(:name => "/managed/location/ny") miq.vms.find(166).tags.assign([{:name => "/managed/location/chicago"}, {:name => "/managed/cc/001"}]) Subcollection resource actions: miq.vms.find(166).tags.find(32).unassign miq.vms.find(166).tags.where(:name => "/managed/location/*").collect(&:unassign) Fixes: #31 Fixes: #32 --- lib/manageiq/api/client.rb | 2 + lib/manageiq/api/client/collection_options.rb | 5 +- lib/manageiq/api/client/resource.rb | 13 +- lib/manageiq/api/client/subcollection.rb | 205 ++++++++++++++++++ lib/manageiq/api/client/subresource.rb | 86 ++++++++ 5 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 lib/manageiq/api/client/subcollection.rb create mode 100644 lib/manageiq/api/client/subresource.rb diff --git a/lib/manageiq/api/client.rb b/lib/manageiq/api/client.rb index 4db91ef..060cb4b 100644 --- a/lib/manageiq/api/client.rb +++ b/lib/manageiq/api/client.rb @@ -25,4 +25,6 @@ require "manageiq/api/client/product_info" require "manageiq/api/client/resource" require "manageiq/api/client/server_info" +require "manageiq/api/client/subcollection" +require "manageiq/api/client/subresource" require "manageiq/api/client/version" diff --git a/lib/manageiq/api/client/collection_options.rb b/lib/manageiq/api/client/collection_options.rb index 81b9643..418bc29 100644 --- a/lib/manageiq/api/client/collection_options.rb +++ b/lib/manageiq/api/client/collection_options.rb @@ -5,11 +5,12 @@ class CollectionOptions attr_reader :attributes attr_reader :virtual_attributes attr_reader :relationships + attr_reader :subcollections attr_reader :data def initialize(options = {}) - @attributes, @virtual_attributes, @relationships, @data = - options.values_at("attributes", "virtual_attributes", "relationships", "data") + @attributes, @virtual_attributes, @relationships, @subcollections, @data = + options.values_at("attributes", "virtual_attributes", "relationships", "subcollections", "data") end end end diff --git a/lib/manageiq/api/client/resource.rb b/lib/manageiq/api/client/resource.rb index 529deff..0bccd77 100644 --- a/lib/manageiq/api/client/resource.rb +++ b/lib/manageiq/api/client/resource.rb @@ -44,13 +44,15 @@ def method_missing(sym, *args, &block) attributes[sym.to_s] elsif action_defined?(sym) exec_action(sym, *args, &block) + elsif subcollection_defined?(sym) + invoke_subcollection(sym) else super end end def respond_to_missing?(sym, *_) - attributes.key?(sym.to_s) || action_defined?(sym) || super + attributes.key?(sym.to_s) || action_defined?(sym) || subcollection_defined?(sym) || super end def exec_action(name, args = nil, &block) @@ -79,6 +81,15 @@ def reload_actions @attributes = resource_hash.except("actions") fetch_actions(resource_hash) end + + def subcollection_defined?(name) + collection.options.subcollections.include?(name.to_s) + end + + def invoke_subcollection(name) + @_subcollections ||= {} + @_subcollections[name.to_s] ||= ManageIQ::API::Client::Subcollection.subclass(name.to_s).new(name.to_s, self) + end end end end diff --git a/lib/manageiq/api/client/subcollection.rb b/lib/manageiq/api/client/subcollection.rb new file mode 100644 index 0000000..ef43957 --- /dev/null +++ b/lib/manageiq/api/client/subcollection.rb @@ -0,0 +1,205 @@ +module ManageIQ + module API + class Client + class Subcollection + include ActionMixin + include Enumerable + include QueryRelation::Queryable + + ACTIONS_RETURNING_RESOURCES = %w(create query).freeze + + CUSTOM_INSPECT_EXCLUSIONS = [:@resource].freeze + include CustomInspectMixin + + attr_reader :name + attr_reader :href + attr_reader :resource + + delegate :client, :to => :resource + + def initialize(name, resource) + @name, @resource, @href = name.to_s, resource, "#{resource.href}/#{name}" + clear_actions + result_hash = client.get(href, :hide => "resources") + fetch_actions(result_hash) + end + + def each(&block) + all.each(&block) + end + + # find(#) returns the object + # find([#]) returns an array of the object + # find(#, #, ...) or find([#, #, ...]) returns an array of the objects + def find(*args) + request_array = args.size == 1 && args[0].kind_of?(Array) + args = args.flatten + case args.size + when 0 + raise "Couldn't find resource without an 'id'" + when 1 + res = limit(1).where(:id => args[0]).to_a + raise "Couldn't find resource with 'id' #{args}" if res.blank? + request_array ? res : res.first + else + raise "Multiple resource find is not supported" unless respond_to?(:query) + query(args.collect { |id| { "id" => id } }) + end + end + + def find_by(args) + limit(1).where(args).first + end + + def pluck(*attrs) + select(*attrs).to_a.pluck(*attrs) + end + + def self.subclass(name) + name = name.camelize + + if const_defined?(name, false) + const_get(name, false) + else + const_set(name, Class.new(self)) + end + end + + def get(options = {}) + options[:expand] = (String(options[:expand]).split(",") | %w(resources)).join(",") + options[:filter] = Array(options[:filter]) if options[:filter].is_a?(String) + result_hash = client.get(href, options) + fetch_actions(result_hash) + klass = ManageIQ::API::Client::Subresource.subclass(name) + result_hash["resources"].collect do |resource_hash| + klass.new(self, resource_hash) + end + end + + def search(mode, options) + options[:limit] = 1 if mode == :first + result = get(parameters_from_query_relation(options)) + case mode + when :first then result.first + when :last then result.last + when :all then result + else raise "Invalid mode #{mode} specified for search" + end + end + + private + + def method_missing(sym, *args, &block) + # get unless actions_present? + if action_defined?(sym) + exec_action(sym, *args, &block) + else + super + end + end + + def respond_to_missing?(sym, *_) + # get unless actions_present? + action_defined?(sym) || super + end + + def parameters_from_query_relation(options) + api_params = {} + [:offset, :limit].each { |opt| api_params[opt] = options[opt] if options[opt] } + api_params[:attributes] = options[:select].join(",") if options[:select].present? + if options[:where] + api_params[:filter] ||= [] + api_params[:filter] += filters_from_query_relation("=", options[:where]) + end + if options[:not] + api_params[:filter] ||= [] + api_params[:filter] += filters_from_query_relation("!=", options[:not]) + end + if options[:order] + order_parameters_from_query_relation(options[:order]).each { |param, value| api_params[param] = value } + end + api_params + end + + def filters_from_query_relation(condition, option) + filters = [] + option.each do |attr, values| + Array(values).each do |value| + value = "'#{value}'" if value.kind_of?(String) && !value.match(/^(NULL|nil)$/i) + filters << "#{attr}#{condition}#{value}" + end + end + filters + end + + def order_parameters_from_query_relation(option) + query_relation_option = + if option.kind_of?(Array) + option.each_with_object({}) { |name, hash| hash[name] = "asc" } + else + option.dup + end + + res_sort_by = [] + res_sort_order = [] + query_relation_option.each do |sort_attr, sort_order| + res_sort_by << sort_attr + sort_order = + case sort_order + when /^asc/i then "asc" + when /^desc/i then "desc" + else raise "Invalid sort order #{sort_order} specified for attribute #{sort_attr}" + end + res_sort_order << sort_order + end + { :sort_by => res_sort_by.join(","), :sort_order => res_sort_order.join(",") } + end + + def exec_action(name, *args, &block) + action = find_action(name) + body = action_body(action.name, *args, &block) + bulk_request = body.key?("resources") + res = client.send(action.method, URI(action.href)) { body } + if ACTIONS_RETURNING_RESOURCES.include?(action.name) && res.key?("results") + klass = ManageIQ::API::Client::Resource.subclass(self.name) + res = results_to_objects(res["results"], klass) + res = res[0] if !bulk_request && res.size == 1 + else + res = res["results"].collect { |result| action_result(result) } + end + res + end + + def results_to_objects(results, klass) + results.collect do |resource_hash| + if ManageIQ::API::Client::ActionResult.an_action_result?(resource_hash) + ManageIQ::API::Client::ActionResult.new(resource_hash) + else + klass.new(self, resource_hash) + end + end + end + + def action_body(action_name, *args, &block) + args = args.flatten + args = args.first if args.size == 1 && args.first.kind_of?(Hash) + args = {} if args.blank? + block_data = block ? block.call : {} + body = { "action" => action_name } + if block_data.present? + if block_data.kind_of?(Array) + body["resources"] = block_data.collect { |resource| resource.merge(args) } + elsif args.present? && args.kind_of?(Array) + body["resources"] = args.collect { |resource| resource.merge(block_data) } + else + body["resource"] = args.dup.merge!(block_data) + end + elsif args.present? + body[args.kind_of?(Array) ? "resources" : "resource"] = args + end + body + end + end + end + end +end diff --git a/lib/manageiq/api/client/subresource.rb b/lib/manageiq/api/client/subresource.rb new file mode 100644 index 0000000..5116a7f --- /dev/null +++ b/lib/manageiq/api/client/subresource.rb @@ -0,0 +1,86 @@ +module ManageIQ + module API + class Client + class Subresource + include ActionMixin + + CUSTOM_INSPECT_EXCLUSIONS = [:@resource].freeze + include CustomInspectMixin + + def self.subclass(name) + name = name.classify + + if const_defined?(name, false) + const_get(name, false) + else + const_set(name, Class.new(self)) + end + end + + attr_reader :attributes + attr_reader :subcollection + attr_reader :actions + + delegate :client, :to => :resource + delegate :resource, :to => :subcollection + + def initialize(subcollection, resource_hash) + raise "Cannot instantiate a Subresource directly" if instance_of?(Subresource) + @subcollection = subcollection + @attributes = resource_hash.except("actions") + add_href + fetch_actions(resource_hash) + end + + def [](attr) + attr_str = attr.to_s + attributes[attr_str] if attributes.key?(attr_str) + end + + private + + def method_missing(sym, *args, &block) + reload_actions unless actions_present? + if attributes.key?(sym.to_s) + attributes[sym.to_s] + elsif action_defined?(sym) + exec_action(sym, *args, &block) + else + super + end + end + + def respond_to_missing?(sym, *_) + attributes.key?(sym.to_s) || action_defined?(sym) || super + end + + def exec_action(name, args = nil, &block) + args ||= {} + raise "Action #{name} parameters must be a hash" if !args.kind_of?(Hash) + action = find_action(name) + res = client.send(action.method, URI(action.href)) do + body = { "action" => action.name } + resource = args.dup + resource.merge!(block.call) if block + resource.present? ? body.merge("resource" => resource) : body + end + action_result(res) + end + + # Let's add href's here if not yet defined by the server + def add_href + return if attributes.key?("href") + return unless attributes.key?("id") + attributes["href"] = "#{resource.href}/#{self.class.name}/#{attributes['id']}" + end + + def reload_actions + return unless attributes.key?("href") + resource_hash = client.get(attributes["href"]) + @attributes = resource_hash.except("actions") + fetch_actions(resource_hash) + end + end + end + end +end