require 'diy/factory.rb' require 'yaml' require 'set' module DIY #:nodoc:# VERSION = '1.1.1' class Context class << self # Enable / disable automatic requiring of libraries. Default: true attr_accessor :auto_require end @auto_require = true # Accepts a Hash defining the object context (usually loaded from objects.yml), and an additional # Hash containing objects to inject into the context. def initialize(context_hash, extra_inputs={}) raise "Nil context hash" unless context_hash raise "Need a hash" unless context_hash.kind_of?(Hash) [ "[]", "keys" ].each do |mname| unless extra_inputs.respond_to?(mname) raise "Extra inputs must respond to hash-like [] operator and methods #keys and #each" end end # store extra inputs if extra_inputs.kind_of?(Hash) @extra_inputs= {} extra_inputs.each { |k,v| @extra_inputs[k.to_s] = v } # smooth out the names else @extra_inputs = extra_inputs end collect_object_and_subcontext_defs context_hash # init the cache @cache = {} @cache['this_context'] = self end # Convenience: create a new DIY::Context by loading from a String (or open file handle.) def self.from_yaml(io_or_string, extra_inputs={}) raise "nil input to YAML" unless io_or_string Context.new(YAML.load(io_or_string), extra_inputs) end # Convenience: create a new DIY::Context by loading from the named file. def self.from_file(fname, extra_inputs={}) raise "nil file name" unless fname self.from_yaml(File.read(fname), extra_inputs) end # Return a reference to the object named. If necessary, the object will # be instantiated on first use. If the object is non-singleton, a new # object will be produced each time. def get_object(obj_name) key = obj_name.to_s obj = @cache[key] unless obj if extra_inputs_has(key) obj = @extra_inputs[key] else case @defs[key] when MethodDef obj = construct_method(key) when FactoryDef obj = construct_factory(key) @cache[key] = obj else obj = construct_object(key) @cache[key] = obj if @defs[key].singleton? end end end obj end alias :[] :get_object # Inject a named object into the Context. This must be done before the Context has instantiated the # object in question. def set_object(obj_name,obj) key = obj_name.to_s raise "object '#{key}' already exists in context" if @cache.keys.include?(key) @cache[key] = obj end alias :[]= :set_object # Provide a listing of object names def keys (@defs.keys.to_set + @extra_inputs.keys.to_set).to_a end # Instantiate and yield the named subcontext def within(sub_context_name) # Find the subcontext definitaion: context_def = @sub_context_defs[sub_context_name.to_s] raise "No sub-context named #{sub_context_name}" unless context_def # Instantiate a new context using self as parent: context = Context.new( context_def, self ) yield context end # Returns true if the context contains an object with the given name def contains_object(obj_name) key = obj_name.to_s @defs.keys.member?(key) or extra_inputs_has(key) end # Every top level object in the Context is instantiated. This is especially useful for # systems that have "floating observers"... objects that are never directly accessed, who # would thus never be instantiated by coincedence. This does not build any subcontexts # that may exist. def build_everything @defs.keys.each { |k| self[k] } end alias :build_all :build_everything alias :preinstantiate_singletons :build_everything private def collect_object_and_subcontext_defs(context_hash) @defs = {} @sub_context_defs = {} get_defs_from context_hash end def get_defs_from(hash, namespace=nil) hash.each do |name,info| # we modify the info hash below so it's important to have a new # instance to play with info = info.dup if info # see if we are building a factory if info and info.has_key?('builds') unless info.has_key?('auto_require') info['auto_require'] = self.class.auto_require end if namespace info['builds'] = namespace.build_classname(info['builds']) end @defs[name] = FactoryDef.new({:name => name, :target => info['builds'], :library => info['library'], :auto_require => info['auto_require']}) next end name = name.to_s case name when /^\+/ # subcontext @sub_context_defs[name.gsub(/^\+/,'')] = info when /^using_namespace/ # namespace: use a module(s) prefix for the classname of contained object defs # NOTE: namespacing is NOT scope... it's just a convenient way to setup class names for a group of objects. get_defs_from info, parse_namespace(name) when /^method\s/ key_name = name.gsub(/^method\s/, "") @defs[key_name] = MethodDef.new(:name => key_name, :object => info['object'], :method => info['method'], :attach => info['attach']) else # Normal object def info ||= {} if extra_inputs_has(name) raise ConstructionError.new(name, "Object definition conflicts with parent context") end unless info.has_key?('auto_require') info['auto_require'] = self.class.auto_require end if namespace if info['class'] info['class'] = namespace.build_classname(info['class']) else info['class'] = namespace.build_classname(name) end end @defs[name] = ObjectDef.new(:name => name, :info => info) end end end def construct_method(key) method_definition = @defs[key] object = get_object(method_definition.object) method = object.method(method_definition.method) unless method_definition.attach.nil? instance_var_name = "@__diy_#{method_definition.object}" method_definition.attach.each do |object_key| get_object(object_key).instance_eval do instance_variable_set(instance_var_name, object) eval %|def #{key}(*args) #{instance_var_name}.#{method_definition.method}(*args) end| end end end return method rescue Exception => oops build_and_raise_construction_error(key, oops) end def construct_object(key) # Find the object definition obj_def = @defs[key] raise "No object definition for '#{key}'" unless obj_def # If object def mentions a library, load it require obj_def.library if obj_def.library # Resolve all components for the object arg_hash = {} obj_def.components.each do |name,value| case value when Lookup arg_hash[name.to_sym] = get_object(value.name) when StringValue arg_hash[name.to_sym] = value.literal_value else raise "Cannot cope with component definition '#{value.inspect}'" end end # Get a reference to the class for the object big_c = get_class_for_name_with_module_delimeters(obj_def.class_name) # Make and return the instance if obj_def.use_class_directly? return big_c elsif arg_hash.keys.size > 0 return big_c.new(arg_hash) else return big_c.new end rescue Exception => oops build_and_raise_construction_error(key, oops) end def build_and_raise_construction_error(key, oops) cerr = ConstructionError.new(key,oops) cerr.set_backtrace(oops.backtrace) raise cerr end def get_class_for_name_with_module_delimeters(class_name) class_name.split(/::/).inject(Object) do |mod,const_name| mod.const_get(const_name) end end def extra_inputs_has(key) if key.nil? or key.strip == '' raise ArgumentError.new("Cannot lookup objects with nil keys") end @extra_inputs.keys.member?(key) or @extra_inputs.keys.member?(key.to_sym) end def parse_namespace(str) Namespace.new(str) end end class Namespace #:nodoc:# def initialize(str) # 'using_namespace Animal Reptile' parts = str.split(/\s+/) raise "Namespace definitions must begin with 'using_namespace'" unless parts[0] == 'using_namespace' parts.shift if parts.length > 0 and parts[0] =~ /::/ parts = parts[0].split(/::/) end raise NamespaceError, "Namespace needs to indicate a module" if parts.empty? @module_nest = parts end def build_classname(name) [ @module_nest, Infl.camelize(name) ].flatten.join("::") end end class Lookup #:nodoc: attr_reader :name def initialize(obj_name) @name = obj_name end end class MethodDef #:nodoc: attr_accessor :name, :object, :method, :attach def initialize(opts) @name, @object, @method, @attach = opts[:name], opts[:object], opts[:method], opts[:attach] end end class ObjectDef #:nodoc: attr_accessor :name, :class_name, :library, :components def initialize(opts) name = opts[:name] raise "Can't make an ObjectDef without a name" if name.nil? info = opts[:info] || {} info = info.clone @components = {} # Object name @name = name # Class name @class_name = info.delete 'class' @class_name ||= info.delete 'type' @class_name ||= Infl.camelize(@name) # Auto Require @auto_require = info.delete 'auto_require' # Library @library = info.delete 'library' @library ||= info.delete 'lib' @library ||= Infl.underscore(@class_name) if @auto_require # Use Class Directly @use_class_directly = info.delete 'use_class_directly' # Auto-compose compose = info.delete 'compose' if compose case compose when Array auto_names = compose.map { |x| x.to_s } when String auto_names = compose.split(',').map { |x| x.to_s.strip } when Symbol auto_names = [ compose.to_s ] else raise "Cannot auto compose object #{@name}, bad 'compose' format: #{compose.inspect}" end end auto_names ||= [] auto_names.each do |cname| @components[cname] = Lookup.new(cname) end # Singleton status if info['singleton'].nil? @singleton = true else @singleton = info['singleton'] end info.delete 'singleton' # Remaining keys info.each do |key,val| @components[key.to_s] = Lookup.new(val.to_s) end end def singleton? @singleton end def use_class_directly? @use_class_directly == true end end class ConstructionError < RuntimeError #:nodoc:# def initialize(object_name, cause=nil) object_name = object_name cause = cause m = "Failed to construct '#{object_name}'" if cause m << "\n ...caused by:\n >>> #{cause}" end super m end end class NamespaceError < RuntimeError #:nodoc:# end module Infl #:nodoc:# # Ganked this from Inflector: def self.camelize(lower_case_and_underscored_word) lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase } end # Ganked this from Inflector: def self.underscore(camel_cased_word) camel_cased_word.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase end end end