esp32-s2_dfu/test/vendor/ceedling/vendor/diy/lib/diy.rb

404 lines
12 KiB
Ruby

require 'diy/factory.rb'
require 'yaml'
require 'set'
module DIY #:nodoc:#
VERSION = '1.1.2'
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