diy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +3 -0
- data/Manifest.txt +53 -0
- data/README.txt +148 -0
- data/Rakefile +17 -0
- data/lib/diy.rb +249 -0
- data/test/diy_test.rb +482 -0
- data/test/files/broken_construction.yml +7 -0
- data/test/files/cat/cat.rb +4 -0
- data/test/files/cat/extra_conflict.yml +5 -0
- data/test/files/cat/heritage.rb +2 -0
- data/test/files/cat/needs_input.yml +3 -0
- data/test/files/cat/the_cat_lineage.rb +1 -0
- data/test/files/dog/dog_model.rb +4 -0
- data/test/files/dog/dog_presenter.rb +4 -0
- data/test/files/dog/dog_view.rb +2 -0
- data/test/files/dog/file_resolver.rb +2 -0
- data/test/files/dog/other_thing.rb +2 -0
- data/test/files/dog/simple.yml +11 -0
- data/test/files/donkey/foo.rb +8 -0
- data/test/files/donkey/foo/bar/qux.rb +7 -0
- data/test/files/fud/objects.yml +13 -0
- data/test/files/fud/toy.rb +15 -0
- data/test/files/gnu/objects.yml +14 -0
- data/test/files/gnu/thinger.rb +8 -0
- data/test/files/goat/base.rb +8 -0
- data/test/files/goat/can.rb +6 -0
- data/test/files/goat/goat.rb +6 -0
- data/test/files/goat/objects.yml +12 -0
- data/test/files/goat/paper.rb +6 -0
- data/test/files/goat/plane.rb +8 -0
- data/test/files/goat/shirt.rb +6 -0
- data/test/files/goat/wings.rb +8 -0
- data/test/files/horse/holder_thing.rb +4 -0
- data/test/files/horse/objects.yml +7 -0
- data/test/files/non_singleton/air.rb +2 -0
- data/test/files/non_singleton/fat_cat.rb +4 -0
- data/test/files/non_singleton/objects.yml +19 -0
- data/test/files/non_singleton/pig.rb +4 -0
- data/test/files/non_singleton/thread_spinner.rb +4 -0
- data/test/files/non_singleton/tick.rb +4 -0
- data/test/files/non_singleton/yard.rb +2 -0
- data/test/files/yak/core_model.rb +4 -0
- data/test/files/yak/core_presenter.rb +4 -0
- data/test/files/yak/core_view.rb +1 -0
- data/test/files/yak/data_source.rb +1 -0
- data/test/files/yak/fringe_model.rb +4 -0
- data/test/files/yak/fringe_presenter.rb +4 -0
- data/test/files/yak/fringe_view.rb +1 -0
- data/test/files/yak/giant_squid.rb +4 -0
- data/test/files/yak/krill.rb +2 -0
- data/test/files/yak/my_objects.yml +21 -0
- data/test/files/yak/sub_sub_context_test.yml +27 -0
- data/test/test_helper.rb +38 -0
- metadata +118 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
lib/diy.rb
|
6
|
+
test/diy_test.rb
|
7
|
+
test/files/broken_construction.yml
|
8
|
+
test/files/cat/cat.rb
|
9
|
+
test/files/cat/extra_conflict.yml
|
10
|
+
test/files/cat/heritage.rb
|
11
|
+
test/files/cat/needs_input.yml
|
12
|
+
test/files/cat/the_cat_lineage.rb
|
13
|
+
test/files/dog/dog_model.rb
|
14
|
+
test/files/dog/dog_presenter.rb
|
15
|
+
test/files/dog/dog_view.rb
|
16
|
+
test/files/dog/file_resolver.rb
|
17
|
+
test/files/dog/other_thing.rb
|
18
|
+
test/files/dog/simple.yml
|
19
|
+
test/files/donkey/foo.rb
|
20
|
+
test/files/donkey/foo/bar/qux.rb
|
21
|
+
test/files/fud/objects.yml
|
22
|
+
test/files/fud/toy.rb
|
23
|
+
test/files/gnu/objects.yml
|
24
|
+
test/files/gnu/thinger.rb
|
25
|
+
test/files/goat/base.rb
|
26
|
+
test/files/goat/can.rb
|
27
|
+
test/files/goat/goat.rb
|
28
|
+
test/files/goat/objects.yml
|
29
|
+
test/files/goat/paper.rb
|
30
|
+
test/files/goat/plane.rb
|
31
|
+
test/files/goat/shirt.rb
|
32
|
+
test/files/goat/wings.rb
|
33
|
+
test/files/horse/holder_thing.rb
|
34
|
+
test/files/horse/objects.yml
|
35
|
+
test/files/non_singleton/air.rb
|
36
|
+
test/files/non_singleton/fat_cat.rb
|
37
|
+
test/files/non_singleton/objects.yml
|
38
|
+
test/files/non_singleton/pig.rb
|
39
|
+
test/files/non_singleton/thread_spinner.rb
|
40
|
+
test/files/non_singleton/tick.rb
|
41
|
+
test/files/non_singleton/yard.rb
|
42
|
+
test/files/yak/core_model.rb
|
43
|
+
test/files/yak/core_presenter.rb
|
44
|
+
test/files/yak/core_view.rb
|
45
|
+
test/files/yak/data_source.rb
|
46
|
+
test/files/yak/fringe_model.rb
|
47
|
+
test/files/yak/fringe_presenter.rb
|
48
|
+
test/files/yak/fringe_view.rb
|
49
|
+
test/files/yak/giant_squid.rb
|
50
|
+
test/files/yak/krill.rb
|
51
|
+
test/files/yak/my_objects.yml
|
52
|
+
test/files/yak/sub_sub_context_test.yml
|
53
|
+
test/test_helper.rb
|
data/README.txt
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
diy
|
2
|
+
|
3
|
+
* http://rubyforge.org/projects/atomicobjectrb/
|
4
|
+
* http://atomicobjectrb.rubyforge.org/diy
|
5
|
+
|
6
|
+
== DESCRIPTION:
|
7
|
+
|
8
|
+
DIY (Dependency Injection in Yaml) is a simple dependency injection library
|
9
|
+
which focuses on declarative composition of objects through constructor injection.
|
10
|
+
|
11
|
+
Currently, all objects that get components put into them must have a
|
12
|
+
constructor that gets a hash with symbols as keys.
|
13
|
+
Best used with constructor.rb
|
14
|
+
|
15
|
+
Auto-naming and auto-library support is done.
|
16
|
+
|
17
|
+
== FEATURES/PROBLEMS:
|
18
|
+
|
19
|
+
* Constructor-based dependency injection container using YAML input.
|
20
|
+
|
21
|
+
== SYNOPSIS:
|
22
|
+
|
23
|
+
=== A Simple Context
|
24
|
+
|
25
|
+
The context is a hash specified in in a yaml file. Each top-level key identifies
|
26
|
+
an object. When the context is created and queried for an object, by default,
|
27
|
+
the context will require a file with the same name:
|
28
|
+
|
29
|
+
require 'foo'
|
30
|
+
|
31
|
+
Next, by default, it will call new on a class from the camel-cased name of the key:
|
32
|
+
|
33
|
+
Foo.new
|
34
|
+
|
35
|
+
foo.rb:
|
36
|
+
class Foo; end
|
37
|
+
|
38
|
+
context.yml:
|
39
|
+
---
|
40
|
+
foo:
|
41
|
+
bar:
|
42
|
+
|
43
|
+
c = DIY::Context.from_file('context.yml')
|
44
|
+
c[:foo] => <Foo:0x81eb0>
|
45
|
+
|
46
|
+
=== Specifying Ruby File to Require
|
47
|
+
|
48
|
+
If the file the class resides in isn't named after they key:
|
49
|
+
|
50
|
+
fun_stuff.rb:
|
51
|
+
class Foo; end
|
52
|
+
|
53
|
+
context.yml:
|
54
|
+
---
|
55
|
+
foo:
|
56
|
+
lib: fun_stuff
|
57
|
+
bar:
|
58
|
+
|
59
|
+
=== Constructor Arguments
|
60
|
+
|
61
|
+
DIY allows specification of constructor arguments as hash key-value pairs
|
62
|
+
using the <tt>compose</tt> directive.
|
63
|
+
|
64
|
+
foo.rb:
|
65
|
+
class Foo
|
66
|
+
def initialize(args)
|
67
|
+
@bar = args[:bar]
|
68
|
+
@other = args[:other]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context.yml:
|
73
|
+
---
|
74
|
+
foo:
|
75
|
+
compose: bar, other
|
76
|
+
bar:
|
77
|
+
other:
|
78
|
+
|
79
|
+
=== Using DIY with constructor.rb:
|
80
|
+
|
81
|
+
foo.rb:
|
82
|
+
class Foo
|
83
|
+
constructor :bar, :other
|
84
|
+
end
|
85
|
+
|
86
|
+
If the constructor argument names don't match up with the object keys
|
87
|
+
in the context, they can be mapped explicitly.
|
88
|
+
|
89
|
+
foo.rb:
|
90
|
+
class Foo
|
91
|
+
constructor :bar, :other
|
92
|
+
end
|
93
|
+
|
94
|
+
context.yml:
|
95
|
+
---
|
96
|
+
foo:
|
97
|
+
bar: my_bar
|
98
|
+
other: the_other_one
|
99
|
+
my_bar:
|
100
|
+
the_other_one:
|
101
|
+
|
102
|
+
=== Non-singleton objects
|
103
|
+
|
104
|
+
Non-singletons will be re-instantiated each time they are needed.
|
105
|
+
|
106
|
+
context.yml:
|
107
|
+
---
|
108
|
+
foo:
|
109
|
+
singleton: false
|
110
|
+
|
111
|
+
bar:
|
112
|
+
|
113
|
+
engine:
|
114
|
+
compose: foo, bar
|
115
|
+
|
116
|
+
== REQUIREMENTS:
|
117
|
+
|
118
|
+
* rubygems
|
119
|
+
* works best with constructor
|
120
|
+
|
121
|
+
== INSTALL:
|
122
|
+
|
123
|
+
* gem install diy
|
124
|
+
|
125
|
+
== LICENSE:
|
126
|
+
|
127
|
+
(The MIT License)
|
128
|
+
|
129
|
+
Copyright (c) 2007 Atomic Object
|
130
|
+
|
131
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
132
|
+
a copy of this software and associated documentation files (the
|
133
|
+
'Software'), to deal in the Software without restriction, including
|
134
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
135
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
136
|
+
permit persons to whom the Software is furnished to do so, subject to
|
137
|
+
the following conditions:
|
138
|
+
|
139
|
+
The above copyright notice and this permission notice shall be
|
140
|
+
included in all copies or substantial portions of the Software.
|
141
|
+
|
142
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
143
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
144
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
145
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
146
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
147
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
148
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hoe'
|
3
|
+
require './lib/diy.rb'
|
4
|
+
|
5
|
+
task :default => [ :test ]
|
6
|
+
|
7
|
+
Hoe.new('diy', DIY::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'atomicobjectrb'
|
9
|
+
p.author = 'Atomic Object'
|
10
|
+
p.email = 'dev@atomicobject.com'
|
11
|
+
p.summary = 'Constructor-based dependency injection container using YAML input.'
|
12
|
+
p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
|
13
|
+
p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
|
14
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
15
|
+
p.test_globs = 'test/*_test.rb'
|
16
|
+
p.extra_deps << ['constructor', '>= 1.0.0']
|
17
|
+
end
|
data/lib/diy.rb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module DIY
|
5
|
+
VERSION = '1.0.0'
|
6
|
+
class Context
|
7
|
+
def initialize(context_hash, extra_inputs={})
|
8
|
+
raise "Nil context hash" unless context_hash
|
9
|
+
raise "Need a hash" unless context_hash.kind_of?(Hash)
|
10
|
+
[ "[]", "keys" ].each do |mname|
|
11
|
+
unless extra_inputs.respond_to?(mname)
|
12
|
+
raise "Extra inputs must respond to hash-like [] operator and methods #keys and #each"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# store extra inputs
|
17
|
+
if extra_inputs.kind_of?(Hash)
|
18
|
+
@extra_inputs = {}
|
19
|
+
extra_inputs.each { |k,v| @extra_inputs[k.to_s] = v } # smooth out the names
|
20
|
+
else
|
21
|
+
@extra_inputs = extra_inputs
|
22
|
+
end
|
23
|
+
|
24
|
+
# Collect object and subcontext definitions
|
25
|
+
@defs = {}
|
26
|
+
@sub_context_defs = {}
|
27
|
+
context_hash.each do |name,info|
|
28
|
+
name = name.to_s
|
29
|
+
case name
|
30
|
+
when /^\+/
|
31
|
+
# subcontext
|
32
|
+
@sub_context_defs[name.gsub(/^\+/,'')] = info
|
33
|
+
|
34
|
+
else
|
35
|
+
# Normal singleton object def
|
36
|
+
if extra_inputs_has(name)
|
37
|
+
raise ConstructionError.new(name, "Object definition conflicts with parent context")
|
38
|
+
end
|
39
|
+
@defs[name] = ObjectDef.new(:name => name, :info => info)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# init the cache
|
45
|
+
@cache = {}
|
46
|
+
@cache['this_context'] = self
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def self.from_yaml(io_or_string, extra_inputs={})
|
51
|
+
raise "nil input to YAML" unless io_or_string
|
52
|
+
Context.new(YAML.load(io_or_string), extra_inputs)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.from_file(fname, extra_inputs={})
|
56
|
+
raise "nil file name" unless fname
|
57
|
+
self.from_yaml(File.read(fname), extra_inputs)
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_object(obj_name)
|
61
|
+
key = obj_name.to_s
|
62
|
+
obj = @cache[key]
|
63
|
+
unless obj
|
64
|
+
if extra_inputs_has(key)
|
65
|
+
obj = @extra_inputs[key]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
unless obj
|
69
|
+
obj = construct_object(key)
|
70
|
+
@cache[key] = obj if @defs[key].singleton?
|
71
|
+
end
|
72
|
+
obj
|
73
|
+
end
|
74
|
+
alias :[] :get_object
|
75
|
+
|
76
|
+
def set_object(obj_name,obj)
|
77
|
+
key = obj_name.to_s
|
78
|
+
raise "object '#{key}' already exists in context" if @cache.keys.include?(key)
|
79
|
+
@cache[key] = obj
|
80
|
+
end
|
81
|
+
alias :[]= :set_object
|
82
|
+
|
83
|
+
def keys
|
84
|
+
(@defs.keys.to_set + @extra_inputs.keys.to_set).to_a
|
85
|
+
end
|
86
|
+
|
87
|
+
# Instantiate and yield the named subcontext
|
88
|
+
def within(sub_context_name)
|
89
|
+
# Find the subcontext definitaion:
|
90
|
+
context_def = @sub_context_defs[sub_context_name.to_s]
|
91
|
+
raise "No sub-context named #{sub_context_name}" unless context_def
|
92
|
+
# Instantiate a new context using self as parent:
|
93
|
+
context = Context.new( context_def, self )
|
94
|
+
|
95
|
+
yield context
|
96
|
+
end
|
97
|
+
|
98
|
+
def contains_object(obj_name)
|
99
|
+
key = obj_name.to_s
|
100
|
+
@defs.keys.member?(key) or extra_inputs_has(key)
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_everything
|
104
|
+
@defs.keys.each { |k| self[k] }
|
105
|
+
end
|
106
|
+
alias :build_all :build_everything
|
107
|
+
alias :preinstantiate_singletons :build_everything
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def construct_object(key)
|
112
|
+
# Find the object definition
|
113
|
+
obj_def = @defs[key]
|
114
|
+
raise "No object definition for '#{key}'" unless obj_def
|
115
|
+
|
116
|
+
# If object def mentions a library, load it
|
117
|
+
require obj_def.library if obj_def.library
|
118
|
+
|
119
|
+
# Resolve all components for the object
|
120
|
+
arg_hash = {}
|
121
|
+
obj_def.components.each do |name,value|
|
122
|
+
case value
|
123
|
+
when Lookup
|
124
|
+
arg_hash[name.to_sym] = get_object(value.name)
|
125
|
+
when StringValue
|
126
|
+
arg_hash[name.to_sym] = value.literal_value
|
127
|
+
else
|
128
|
+
raise "Cannot cope with component definition '#{value.inspect}'"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
# Get a reference to the class for the object
|
132
|
+
big_c = get_class_for_name_with_module_delimeters(obj_def.class_name)
|
133
|
+
# Make and return the instance
|
134
|
+
if arg_hash.keys.size > 0
|
135
|
+
return big_c.new(arg_hash)
|
136
|
+
else
|
137
|
+
return big_c.new
|
138
|
+
end
|
139
|
+
rescue Exception => oops
|
140
|
+
cerr = ConstructionError.new(key,oops)
|
141
|
+
cerr.set_backtrace(oops.backtrace)
|
142
|
+
raise cerr
|
143
|
+
end
|
144
|
+
|
145
|
+
def get_class_for_name_with_module_delimeters(class_name)
|
146
|
+
class_name.split(/::/).inject(Object) do |mod,const_name| mod.const_get(const_name) end
|
147
|
+
end
|
148
|
+
|
149
|
+
def extra_inputs_has(key)
|
150
|
+
if key.nil? or key.strip == ''
|
151
|
+
raise ArgumentError.new("Cannot lookup objects with nil keys")
|
152
|
+
end
|
153
|
+
@extra_inputs.keys.member?(key) or @extra_inputs.keys.member?(key.to_sym)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class Lookup #:nodoc:
|
158
|
+
attr_reader :name
|
159
|
+
def initialize(obj_name)
|
160
|
+
@name = obj_name
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class ObjectDef #:nodoc:
|
165
|
+
attr_accessor :name, :class_name, :library, :components
|
166
|
+
def initialize(opts)
|
167
|
+
name = opts[:name]
|
168
|
+
raise "Can't make an ObjectDef without a name" if name.nil?
|
169
|
+
|
170
|
+
info = opts[:info] || {}
|
171
|
+
info = info.clone
|
172
|
+
|
173
|
+
@components = {}
|
174
|
+
|
175
|
+
# Object name
|
176
|
+
@name = name
|
177
|
+
|
178
|
+
# Class name
|
179
|
+
@class_name = info.delete 'class'
|
180
|
+
@class_name ||= info.delete 'type'
|
181
|
+
@class_name ||= camelize(@name)
|
182
|
+
|
183
|
+
# Library
|
184
|
+
@library = info.delete 'library'
|
185
|
+
@library ||= info.delete 'lib'
|
186
|
+
@library ||= underscore(@class_name)
|
187
|
+
|
188
|
+
# Auto-compose
|
189
|
+
compose = info.delete 'compose'
|
190
|
+
if compose
|
191
|
+
case compose
|
192
|
+
when Array
|
193
|
+
auto_names = compose.map { |x| x.to_s }
|
194
|
+
when String
|
195
|
+
auto_names = compose.split(',').map { |x| x.to_s.strip }
|
196
|
+
when Symbol
|
197
|
+
auto_names = [ compose.to_s ]
|
198
|
+
else
|
199
|
+
raise "Cannot auto compose object #{@name}, bad 'compose' format: #{compose.inspect}"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
auto_names ||= []
|
203
|
+
auto_names.each do |cname|
|
204
|
+
@components[cname] = Lookup.new(cname)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Singleton status
|
208
|
+
if info['singleton'].nil?
|
209
|
+
@singleton = true
|
210
|
+
else
|
211
|
+
@singleton = info['singleton']
|
212
|
+
end
|
213
|
+
info.delete 'singleton'
|
214
|
+
|
215
|
+
# Remaining keys
|
216
|
+
info.each do |key,val|
|
217
|
+
@components[key.to_s] = Lookup.new(val.to_s)
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
def singleton?
|
223
|
+
@singleton
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
# Ganked this from Inflector:
|
228
|
+
def camelize(lower_case_and_underscored_word)
|
229
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
230
|
+
end
|
231
|
+
# Ganked this from Inflector:
|
232
|
+
def underscore(camel_cased_word)
|
233
|
+
camel_cased_word.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Exception raised when an object can't be created which is defined in the context.
|
238
|
+
class ConstructionError < RuntimeError
|
239
|
+
def initialize(object_name, cause=nil) #:nodoc:
|
240
|
+
object_name = object_name
|
241
|
+
cause = cause
|
242
|
+
m = "Failed to construct '#{object_name}'"
|
243
|
+
if cause
|
244
|
+
m << "\n ...caused by:\n >>> #{cause}"
|
245
|
+
end
|
246
|
+
super m
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|