require 'virtualbox/abstract_model/version_matcher' module VirtualBox class AbstractModel # Module which can be included into any other class which allows # that class to have attributes using the "attribute" class method. # This creates the reader/writer for the attribute but also provides # other useful options such as readonly attributes, default values, # and more. # # Make sure to also see the {ClassMethods}. # # ## Defining a Basic Attribute # # attribute :name # # The example above would put the "name" attribute on the class. This # would give the class the following abilities # # instance.name = "Harry!" # puts instance.name # => "Harry!" # # Basic attributes alone are not different than ruby's built-in # `attr_*` methods. # # ## Defining a Readonly Attribute # # attribute :age, :readonly => true # # The example above allows age to be read, but not written to via the # `age=` method. The attribute is still able to written using # {#write_attribute} but this is generally only for # inter-class use, and not for users of it. # # ## Defining Default Values # # attribute :format, :default => "VDI" # # The example above applies a default value to format. So if no value # is ever given to it, `format` would return `VDI`. # # ## Attributes for a Specific VirtualBox Version # # Attributes change with different VirtualBox versions. One way to # provide version-specific behavior is to specify the version # which the attribute applies to. # # attribute :name, :version => "3.2" # attribute :age, :version => "3.1" # # These versions only apply to major and minor releases of # VirtualBox. Patch releases can't be specified (such as "3.2.4") # # ## Populating Multiple Attributes # # Attributes can be mass populated using {#populate_attributes}. Below # is an example of the use. # # class Person # include Attributable # # attribute :name # attribute :age, :readonly => true # # def initialize # populate_attributes({ # :name => "Steven", # :age => 27 # }) # end # end # # **Note:** Populating attributes is not the same as mass-updating attributes. # {#populate_attributes} is meant to do initial population only. There is # currently no method for mass assignment for updating. # # ## Custom Populate Keys # # Sometimes the attribute names don't match the keys of the hash that will be # used to populate it. For this purpose, you can define a custom # `populate_key`. Example: # # attribute :path, :populate_key => :location # # def initialize # populate_attributes(:location => "Home") # puts path # => "Home" # end # # ## Lazy Loading Attributes # # While most attributes are fairly trivial to calculate and populate, sometimes # attributes may have an expensive cost to populate, and are generally not worth # populating unless a user of the class requests that attribute. This is known as # _lazy loading_ the attributes. This is possibly by specifying the `:lazy` option # on the attribute. In this case, the first time (and _only_ the first time) the # attribute is requested, `load_attribute` will be called with the name of the # attribute as the parameter. This method is then expected to call `write_attribute` # on that attribute to give it a value. # # class ExpensiveAttributeModel # include VirtualBox::AbstractModel::Attributable # attribute :expensive_attribute, :lazy => true # # def load_attribute(name) # if name == :expensive_attribute # write_attribute(name, perform_expensive_calculation) # end # end # end # # Using the above definition, we could use the class like so: # # # Initializing is fast, since no attribute population is done # model = ExpensiveAttributeModel.new # # # But this is slow, since it has to calculate. # puts model.expensive_attribute # # # But ONLY THE FIRST TIME. This time is FAST! # puts model.expensive_attribute # # In addition to calling `load_attribute` on initial read, `write_attribute` # when performed on a lazy loaded attribute will mark it as "loaded" so there # will be no load called on the first request. Example, using the above class # once again: # # model = ExpensiveAttributeModel.new # model.write_attribute(:expensive_attribute, 42) # # # This is FAST, since "load_attribute" is not called # puts model.expensive_attribute # => 42 # module Attributable include VersionMatcher def self.included(base) base.extend ClassMethods end # Defines the class methods for the {Attributable} module. For # detailed overview documentation, see {Attributable}. module ClassMethods # Defines an attribute on the model. # # @param [Symbol] name The name of the attribute, which will also be # used to set the accessor methods. # @option options [Boolean] :readonly (false) If true, attribute will be readonly. # More specifically, the `attribute=` method won't be defined for it. # @option options [Object] :default (nil) Specifies a default value for the # attribute. # @option options [Symbol] :populate_key (attribute name) Specifies # a custom populate key to use for {Attributable#populate_attributes} def attribute(name, options = {}) name = name.to_sym attributes[name] = defined?(@__attribute_scope) ? @__attribute_scope.dup : {} attributes[name].merge!(options) # Create the method for reading this attribute define_method("#{name}?") { read_attribute(name) } if options[:boolean] define_method(name) { read_attribute(name) } # Create the writer method for it unless the attribute is readonly, # then remove the method if it exists if !options[:readonly] define_method("#{name}=") do |value| write_attribute(name, value) end elsif method_defined?("#{name}=") undef_method("#{name}=") end end # Defines the specified scope for all attributes within the block. # The scope is reset to the previous value once the block ends. Multiple # scopes can be nested and they'll inherit from each other. def attribute_scope(options, &block) @__attribute_scope ||= {} old_value = @__attribute_scope @__attribute_scope = old_value.merge(options) instance_eval(&block) @__attribute_scope = old_value end # Returns the hash of attributes and their associated options. def attributes @attributes ||= {} end # Used to propagate attributes to subclasses. This method makes sure that # subclasses of a class with {Attributable} included will inherit the # attributes as well, which would be the expected behaviour. def inherited(subclass) super rescue NoMethodError attributes.each do |name, option| subclass.attribute(name, option) end end end # Does the initial population of the various attributes. It will # ignore attributes which are not defined or have no value in the # hash. # # Population uses the attributes `populate_key` if present to # determine which value to take. Example: # # attribute :name, :populate_key => :namae # attribute :age # # def initialize # populate_attributes(:namae => "Henry", :age => 27) # end # # The above example would set `name` to `Henry` since that is # the `populate_key`. If a `populate_key` is not present, the # attribute name is used. def populate_attributes(attribs) self.class.attributes.each do |key, options| value_key = options[:populate_key] || key write_attribute(key, attribs[value_key]) if attribs.has_key?(value_key) end end # Reset attributes which basically causes all lazy attributes to # be reset. This forces them to reload when they're accessed. def reset_attributes # Find the lazy attributes to_delete = [] attributes.each do |name, value| to_delete << name if lazy_attribute?(name) end # Delete them! to_delete.each { |key| attributes.delete(key) } end # Writes an attribute. This method ignores the `readonly` option # on attribute definitions. This method is mostly meant for # internal use on setting attributes (including readonly # attributes), whereas users of a class which includes this # module should use the accessor methods, such as `name=`. def write_attribute(name, value) attributes[name] = value end # Reads an attribute. This method will return `nil` if the # attribute doesn't exist. If the attribute does exist but # doesn't have a value set, it'll use the `default` value # if specified. def read_attribute(name) if has_attribute?(name) options = self.class.attributes[name] assert_version_match(options[:version], VirtualBox.version) if options[:version] if lazy_attribute?(name) && !loaded_attribute?(name) # Load the lazy attribute attributes[name] = load_attribute(name.to_sym) end if attributes[name].nil? options[:default] else attributes[name] end end end # Returns a hash of all attributes and their options. def attributes @attribute_values ||= {} end # Returns boolean value denoting if an attribute exists. def has_attribute?(name) self.class.attributes.has_key?(name.to_sym) end # Returns boolean value denoting if an attribute is "lazy loaded" def lazy_attribute?(name) has_attribute?(name) && self.class.attributes[name.to_sym][:lazy] end # Returns boolean value denoting if an attribute has been loaded # yet. def loaded_attribute?(name) attributes.has_key?(name) end # Returns a boolean value denoting if an attribute is readonly. # This method also returns false for **nonexistent attributes** # so it should be used in conjunction with {#has_attribute?} if # existence is important. def readonly_attribute?(name) name = name.to_sym has_attribute?(name) && self.class.attributes[name][:readonly] end end end end