mirror of
https://github.com/hathach/tinyusb.git
synced 2025-01-17 05:32:55 +08:00
bc735bbe22
- unified makefile project for the whole repos - new separate project for tests
404 lines
12 KiB
Ruby
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
|