From: Michal Fojtik <[email protected]> - CIMI::Model::Resource moved from base.rb to separate file - The $select methods moved to a separate module - CIMI::Model::Collection helpers now extend the Base class without reopening it (extend CollectionMethods) - CIMI::Model::Base methods layout reorganized to be more readable
Signed-off-by: Michal fojtik <[email protected]> --- server/lib/cimi/helpers/select_helper.rb | 62 ++++++++ server/lib/cimi/models.rb | 23 ++- server/lib/cimi/models/base.rb | 241 ++----------------------------- server/lib/cimi/models/collection.rb | 124 ++++++++-------- server/lib/cimi/models/resource.rb | 189 ++++++++++++++++++++++++ 5 files changed, 347 insertions(+), 292 deletions(-) create mode 100644 server/lib/cimi/helpers/select_helper.rb create mode 100644 server/lib/cimi/models/resource.rb diff --git a/server/lib/cimi/helpers/select_helper.rb b/server/lib/cimi/helpers/select_helper.rb new file mode 100644 index 0000000..59ce034 --- /dev/null +++ b/server/lib/cimi/helpers/select_helper.rb @@ -0,0 +1,62 @@ +# 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 CIMI + module Helpers + module SelectResourceMethods + + def select_by(filter_opts) + return self if filter_opts.nil? + return self unless kind_of? CIMI::Model::Collection + if filter_opts.include? ',' + return select_attributes(filter_opts.split(',').map{ |a| a.intern }) + end + case filter_opts + when /^([\w\_]+)$/ then select_attributes([$1.intern]) + when /^([\w\_]+)\[(\d+\-\d+)\]$/ then select_by_arr_range($1.intern, $2) + when /^([\w\_]+)\[(\d+)\]$/ then select_by_arr_index($1.intern, $2) + else self + end + end + + def select_by_arr_index(attr, filter) + return self unless self.respond_to?(attr) + self.class.new(attr => [self.send(attr)[filter.to_i]]) + end + + def select_by_arr_range(attr, filter) + return self unless self.respond_to?(attr) + filter = filter.split('-').inject { |s,e| s.to_i..e.to_i } + self.class.new(attr => self.send(attr)[filter]) + end + + end + + module SelectBaseMethods + def select_attributes(attr_list) + attrs = attr_list.inject({}) do |result, attr| + attr = attr.to_s.underscore + result[attr.to_sym] = self.send(attr) if self.respond_to?(attr) + result + end + self.class.new(attrs.merge( + :select_attr_list => attr_list, + :base_id => self.send(:id) + )) + end + end + + end +end diff --git a/server/lib/cimi/models.rb b/server/lib/cimi/models.rb index 356f5a7..20c0ef1 100644 --- a/server/lib/cimi/models.rb +++ b/server/lib/cimi/models.rb @@ -13,8 +13,26 @@ # License for the specific language governing permissions and limitations # under the License. # + module CIMI - module Model; end + module Model + def self.register_as_root_entity!(klass, opts = {}) + @root_entities ||= [CIMI::Model::CloudEntryPoint] + @root_entities << klass + name = klass.name.split("::").last.pluralize + unless CIMI::Model::CloudEntryPoint.href_defined?(name) + params = {} + if opts[:as] + params[:xml_name] = params[:json_name] = opts[:as] + end + CIMI::Model::CloudEntryPoint.send(:href, name.underscore, params) + end + end + + def self.root_entities + @root_entities || [] + end + end end require 'require_relative' if RUBY_VERSION < '1.9' @@ -31,8 +49,9 @@ unless Deltacloud.test_environment? end require_relative './models/schema' -require_relative './models/base' +require_relative './models/resource' require_relative './models/collection' +require_relative './models/base' require_relative './models/errors' require_relative './models/action' require_relative './models/machine_volume' diff --git a/server/lib/cimi/models/base.rb b/server/lib/cimi/models/base.rb index c1ed579..c74b8c3 100644 --- a/server/lib/cimi/models/base.rb +++ b/server/lib/cimi/models/base.rb @@ -15,6 +15,8 @@ require 'xmlsimple' +require_relative '../helpers/database_helper' + # The base class for any CIMI object that we either read from a request or # write as a response. This class handles serializing/deserializing XML and # JSON into a common form. @@ -71,238 +73,21 @@ require 'xmlsimple' module CIMI::Model - def self.register_as_root_entity!(klass, opts = {}) - @root_entities ||= [CIMI::Model::CloudEntryPoint] - @root_entities << klass - name = klass.name.split("::").last.pluralize - unless CIMI::Model::CloudEntryPoint.href_defined?(name) - params = {} - if opts[:as] - params[:xml_name] = params[:json_name] = opts[:as] - end - CIMI::Model::CloudEntryPoint.send(:href, name.underscore, params) - end - end - - def self.root_entities - @root_entities || [] - end - -end - -class CIMI::Model::Resource - - # - # We keep the values of the attributes in a hash - # - attr_reader :attribute_values - - CMWG_NAMESPACE = "http://schemas.dmtf.org/cimi/1" - - # Keep the list of all attributes in an array +attributes+; for each - # attribute, we also define a getter and a setter to access/change the - # value for that attribute - class << self - - def <<(model) - clone_base_schema unless base_schema_cloned? - member_name = model.name.split("::").last - if ::Struct.const_defined?("CIMI_#{member_name}") - ::Struct.send(:remove_const, "CIMI_#{member_name}") - end - member_symbol = member_name.underscore.pluralize.to_sym - members = CIMI::Model::Schema::Array.new(member_symbol) - members.struct.schema.attributes = model.schema.attributes - base_schema.attributes << members - end - - def base_schema - @schema ||= CIMI::Model::Schema.new - end - - def clone_base_schema - @schema_duped = true - @schema = Marshal::load(Marshal.dump(superclass.base_schema)) - end - - def base_schema_cloned? - @schema_duped - end + class Base < Resource - private :clone_base_schema, :base_schema_cloned? + # Extend the base model with database methods + extend Deltacloud::Helpers::Database - def inherited(child) - child.instance_eval do - def schema - base_schema_cloned? ? @schema : clone_base_schema - end - end - end + # Extend the base model with the collection handling methods + extend CIMI::Model::CollectionMethods - def add_attributes!(names, attr_klass, &block) - if self.respond_to? :schema - schema.add_attributes!(names, attr_klass, &block) - else - base_schema.add_attributes!(names, attr_klass, &block) - end - names.each do |name| - define_method(name) { self[name] } - define_method(:"#{name}=") { |newval| self[name] = newval } - end - end - - # Return Array of links to current CIMI object - def all_uri(context) - self.all(context).map { |e| { :href => e.id } } - end - end - - extend CIMI::Model::Schema::DSL - - def [](a) - @attribute_values[a] - end - - def []=(a, v) - return @attribute_values.delete(a) if v.nil? - @attribute_values[a] = self.class.schema.convert(a, v) - end - - # Prepare to serialize - def prepare - self.class.schema.collections.map { |coll| coll.name }.each do |n| - if @select_attrs.empty? or @select_attrs.include?(n) - self[n].href = "#{self.base_id}/#{n}" if !self[n].href - self[n].id = "#{self.base_id}/#{n}" if !self[n].entries.empty? - else - self[n] = nil - end - end - end - - # - # Factory methods - # - def initialize(values = {}) - names = self.class.schema.attribute_names - @select_attrs = values[:select_attr_list] || [] - # Make sure we always have the :id of the entity even - # the $select parameter is used and :id is filtered out + # Include methods needed to handle the $select query parameter + include CIMI::Helpers::SelectBaseMethods # - @base_id = values[:base_id] || values[:id] - @attribute_values = names.inject(OrderedHash.new) do |hash, name| - hash[name] = self.class.schema.convert(name, values[name]) - hash - end - end - - def base_id - self.id || @base_id - end - - # Construct a new object from the XML representation +xml+ - def self.from_xml(text) - xml = XmlSimple.xml_in(text, :force_content => true) - model = self.new - @schema.from_xml(xml, model) - model - end - - # Construct a new object - def self.from_json(text) - json = JSON::parse(text) - model = self.new - @schema.from_json(json, model) - model - end - - def self.parse(text, content_type) - if content_type == "application/xml" - from_xml(text) - elsif content_type == "application/json" - from_json(text) - else - raise "Can not parse content type #{content_type}" - end - end - - # - # Serialize - # - - def self.xml_tag_name - self.name.split("::").last - end - - def self.resource_uri - CMWG_NAMESPACE + "/" + self.name.split("::").last - end - - def self.to_json(model) - json = @schema.to_json(model) - json[:resourceURI] = resource_uri - JSON::unparse(json) - end - - def self.to_xml(model) - xml = @schema.to_xml(model) - xml["xmlns"] = CMWG_NAMESPACE - xml["resourceURI"] = resource_uri - XmlSimple.xml_out(xml, :root_name => xml_tag_name) - end - - def to_json - self.class.to_json(self) - end - - def to_xml - self.class.to_xml(self) - end - - def select_by(filter_opts) - return self if filter_opts.nil? - return select_attributes(filter_opts.split(',').map{ |a| a.intern }) if filter_opts.include? ',' - case filter_opts - when /^([\w\_]+)$/ then select_attributes([$1.intern]) - when /^([\w\_]+)\[(\d+\-\d+)\]$/ then select_by_arr_range($1.intern, $2) - when /^([\w\_]+)\[(\d+)\]$/ then select_by_arr_index($1.intern, $2) - else self - end - end - - def select_by_arr_index(attr, filter) - return self unless self.respond_to?(attr) - self.class.new(attr => [self.send(attr)[filter.to_i]]) - end - - def select_by_arr_range(attr, filter) - return self unless self.respond_to?(attr) - filter = filter.split('-').inject { |s,e| s.to_i..e.to_i } - self.class.new(attr => self.send(attr)[filter]) - end -end - -require_relative '../helpers/database_helper' - -class CIMI::Model::Base < CIMI::Model::Resource - extend Deltacloud::Helpers::Database - # - # Common attributes for all resources - # - text :id, :name, :description, :created - - hash :property - - def select_attributes(attr_list) - attrs = attr_list.inject({}) do |result, attr| - attr = attr.to_s.underscore - result[attr.to_sym] = self.send(attr) if self.respond_to?(attr) - result - end - self.class.new(attrs.merge( - :select_attr_list => attr_list, - :base_id => self.send(:id) - )) + # Common attributes for all resources + # + text :id, :name, :description, :created + hash :property end end diff --git a/server/lib/cimi/models/collection.rb b/server/lib/cimi/models/collection.rb index 931adcc..f36c081 100644 --- a/server/lib/cimi/models/collection.rb +++ b/server/lib/cimi/models/collection.rb @@ -19,10 +19,36 @@ module CIMI::Model class << self attr_accessor :entry_name, :embedded + + def xml_tag_name + 'Collection' + end + + def generate(model_class, opts = {}) + model_name = model_class.name.split("::").last + scope = opts[:scope] || CIMI::Model + coll_class = Class.new(CIMI::Model::Collection) + scope.const_set(:"#{model_name}Collection", coll_class) + coll_class.entry_name = model_name.underscore.pluralize.to_sym + coll_class.embedded = opts[:embedded] + entry_schema = model_class.schema + coll_class.instance_eval do + text :id + scalar :href + text :count + scalar :href if opts[:embedded] + array self.entry_name, :schema => entry_schema, :xml_name => model_name + array :operations do + scalar :rel, :href + end + end + coll_class + end + end # Make sure the base schema gets cloned - self.schema + schema # You can initialize collection by passing the Hash representation of the # collection or passing another Collection object. @@ -76,76 +102,50 @@ module CIMI::Model end self end - - def self.xml_tag_name - "Collection" - end - - def self.generate(model_class, opts = {}) - model_name = model_class.name.split("::").last - scope = opts[:scope] || CIMI::Model - coll_class = Class.new(CIMI::Model::Collection) - scope.const_set(:"#{model_name}Collection", coll_class) - coll_class.entry_name = model_name.underscore.pluralize.to_sym - coll_class.embedded = opts[:embedded] - entry_schema = model_class.schema - coll_class.instance_eval do - text :id - scalar :href - text :count - scalar :href if opts[:embedded] - array self.entry_name, :schema => entry_schema, :xml_name => model_name - array :operations do - scalar :rel, :href - end - end - coll_class - end end - # - # We need to reopen Base and add some stuff to avoid circular dependencies - # - class Base - # - # Toplevel collections - # + module CollectionMethods - class << self + def collection_class=(klass) + @collection_class = klass + end - attr_accessor :collection_class + def collection_class + @collection_class + end - def acts_as_root_entity(opts = {}) - self.collection_class = Collection.generate(self) - CIMI::Model.register_as_root_entity! self, opts - end + def acts_as_root_entity(opts = {}) + self.collection_class = Collection.generate(self) + CIMI::Model.register_as_root_entity! self, opts + end - # Return a collection of entities - def list(context) - entries = find(:all, context) - desc = "#{self.name.split("::").last} Collection for the #{context.driver.name.capitalize} driver" - acts_as_root_entity unless collection_class - id = context.send("#{collection_class.entry_name}_url") - ops = [] - cimi_entity = collection_class.entry_name.to_s.singularize - cimi_create = "create_#{cimi_entity}_url" - dcloud_create = context.deltacloud_create_method_for(cimi_entity) - if(context.respond_to?(cimi_create) && - context.driver.respond_to?(dcloud_create)) || - provides?(cimi_entity) - url = context.send(cimi_create) - ops << { :rel => "add", :href => url } - end - collection_class.new(:id => id, - :count => entries.size, - :entries => entries, - :operations => ops, - :description => desc) + # Return a collection of entities + def list(context) + entries = find(:all, context) + desc = "#{self.name.split("::").last} Collection for the #{context.driver.name.capitalize} driver" + acts_as_root_entity unless collection_class + id = context.send("#{collection_class.entry_name}_url") + ops = [] + cimi_entity = collection_class.entry_name.to_s.singularize + cimi_create = "create_#{cimi_entity}_url" + dcloud_create = context.deltacloud_create_method_for(cimi_entity) + if(context.respond_to?(cimi_create) && + context.driver.respond_to?(dcloud_create)) || + provides?(cimi_entity) + url = context.send(cimi_create) + ops << { :rel => "add", :href => url } end + collection_class.new(:id => id, + :count => entries.size, + :entries => entries, + :operations => ops, + :description => desc) end - def self.all(context) - find(:all, context) + def all(context) + find :all, context end + end + end diff --git a/server/lib/cimi/models/resource.rb b/server/lib/cimi/models/resource.rb new file mode 100644 index 0000000..e99e1a2 --- /dev/null +++ b/server/lib/cimi/models/resource.rb @@ -0,0 +1,189 @@ +# 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_relative '../helpers/select_helper' + +module CIMI + module Model + class Resource + + extend CIMI::Model::Schema::DSL + include CIMI::Helpers::SelectResourceMethods + + # + # We keep the values of the attributes in a hash + # + attr_reader :attribute_values + + CMWG_NAMESPACE = "http://schemas.dmtf.org/cimi/1" + + # + # Factory methods + # + def initialize(values = {}) + names = self.class.schema.attribute_names + @select_attrs = values[:select_attr_list] || [] + # Make sure we always have the :id of the entity even + # the $select parameter is used and :id is filtered out + # + @base_id = values[:base_id] || values[:id] + @attribute_values = names.inject(OrderedHash.new) do |hash, name| + hash[name] = self.class.schema.convert(name, values[name]) + hash + end + end + + # The CIMI::Model::Resource class methods + class << self + + def base_schema + @schema ||= CIMI::Model::Schema.new + end + + def clone_base_schema + @schema_duped = true + @schema = Marshal::load(Marshal.dump(superclass.base_schema)) + end + + def base_schema_cloned? + @schema_duped + end + + private :clone_base_schema, :base_schema_cloned? + + # If the model is inherited by another model, we want to clone + # the base schema instead of using the parent model schema, which + # might be modified + # + def inherited(child) + child.instance_eval do + def schema + base_schema_cloned? ? @schema : clone_base_schema + end + end + end + + def add_attributes!(names, attr_klass, &block) + if self.respond_to? :schema + schema.add_attributes!(names, attr_klass, &block) + else + base_schema.add_attributes!(names, attr_klass, &block) + end + names.each do |name| + define_method(name) { self[name] } + define_method(:"#{name}=") { |newval| self[name] = newval } + end + end + + # Return Array of links to current CIMI object + # + def all_uri(context) + self.all(context).map { |e| { :href => e.id } } + end + + # Construct a new object from the XML representation +xml+ + def from_xml(text) + xml = XmlSimple.xml_in(text, :force_content => true) + model = self.new + @schema.from_xml(xml, model) + model + end + + # Construct a new object + def from_json(text) + json = JSON::parse(text) + model = self.new + @schema.from_json(json, model) + model + end + + def parse(text, content_type) + if content_type == "application/xml" + from_xml(text) + elsif content_type == "application/json" + from_json(text) + else + raise "Can not parse content type #{content_type}" + end + end + + # + # Serialize + # + + def xml_tag_name + self.name.split("::").last + end + + def resource_uri + CMWG_NAMESPACE + "/" + self.name.split("::").last + end + + def to_json(model) + json = @schema.to_json(model) + json[:resourceURI] = resource_uri + JSON::unparse(json) + end + + def to_xml(model) + xml = @schema.to_xml(model) + xml["xmlns"] = CMWG_NAMESPACE + xml["resourceURI"] = resource_uri + XmlSimple.xml_out(xml, :root_name => xml_tag_name) + end + end + + # END of class methods + + def [](a) + @attribute_values[a] + end + + def []=(a, v) + return @attribute_values.delete(a) if v.nil? + @attribute_values[a] = self.class.schema.convert(a, v) + end + + # Apply the $select options to all sub-collections and prepare then + # to serialize by setting correct :href and :id attributes. + # + def prepare + self.class.schema.collections.map { |coll| coll.name }.each do |n| + if @select_attrs.empty? or @select_attrs.include?(n) + self[n].href = "#{self.base_id}/#{n}" if !self[n].href + self[n].id = "#{self.base_id}/#{n}" if !self[n].entries.empty? + else + self[n] = nil + end + end + end + + def base_id + self.id || @base_id + end + + def to_json + self.class.to_json(self) + end + + def to_xml + self.class.to_xml(self) + end + + end + + end +end + -- 1.8.1
