Skip to content

Commit

Permalink
Adding support for subcollections and related subresources
Browse files Browse the repository at this point in the history
- 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: ManageIQ#31
Fixes: ManageIQ#32
  • Loading branch information
abellotti committed Apr 2, 2018
1 parent c78c188 commit a21b621
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 3 deletions.
2 changes: 2 additions & 0 deletions lib/manageiq/api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
5 changes: 3 additions & 2 deletions lib/manageiq/api/client/collection_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion lib/manageiq/api/client/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
205 changes: 205 additions & 0 deletions lib/manageiq/api/client/subcollection.rb
Original file line number Diff line number Diff line change
@@ -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
86 changes: 86 additions & 0 deletions lib/manageiq/api/client/subresource.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a21b621

Please sign in to comment.