diy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/History.txt +3 -0
  2. data/Manifest.txt +53 -0
  3. data/README.txt +148 -0
  4. data/Rakefile +17 -0
  5. data/lib/diy.rb +249 -0
  6. data/test/diy_test.rb +482 -0
  7. data/test/files/broken_construction.yml +7 -0
  8. data/test/files/cat/cat.rb +4 -0
  9. data/test/files/cat/extra_conflict.yml +5 -0
  10. data/test/files/cat/heritage.rb +2 -0
  11. data/test/files/cat/needs_input.yml +3 -0
  12. data/test/files/cat/the_cat_lineage.rb +1 -0
  13. data/test/files/dog/dog_model.rb +4 -0
  14. data/test/files/dog/dog_presenter.rb +4 -0
  15. data/test/files/dog/dog_view.rb +2 -0
  16. data/test/files/dog/file_resolver.rb +2 -0
  17. data/test/files/dog/other_thing.rb +2 -0
  18. data/test/files/dog/simple.yml +11 -0
  19. data/test/files/donkey/foo.rb +8 -0
  20. data/test/files/donkey/foo/bar/qux.rb +7 -0
  21. data/test/files/fud/objects.yml +13 -0
  22. data/test/files/fud/toy.rb +15 -0
  23. data/test/files/gnu/objects.yml +14 -0
  24. data/test/files/gnu/thinger.rb +8 -0
  25. data/test/files/goat/base.rb +8 -0
  26. data/test/files/goat/can.rb +6 -0
  27. data/test/files/goat/goat.rb +6 -0
  28. data/test/files/goat/objects.yml +12 -0
  29. data/test/files/goat/paper.rb +6 -0
  30. data/test/files/goat/plane.rb +8 -0
  31. data/test/files/goat/shirt.rb +6 -0
  32. data/test/files/goat/wings.rb +8 -0
  33. data/test/files/horse/holder_thing.rb +4 -0
  34. data/test/files/horse/objects.yml +7 -0
  35. data/test/files/non_singleton/air.rb +2 -0
  36. data/test/files/non_singleton/fat_cat.rb +4 -0
  37. data/test/files/non_singleton/objects.yml +19 -0
  38. data/test/files/non_singleton/pig.rb +4 -0
  39. data/test/files/non_singleton/thread_spinner.rb +4 -0
  40. data/test/files/non_singleton/tick.rb +4 -0
  41. data/test/files/non_singleton/yard.rb +2 -0
  42. data/test/files/yak/core_model.rb +4 -0
  43. data/test/files/yak/core_presenter.rb +4 -0
  44. data/test/files/yak/core_view.rb +1 -0
  45. data/test/files/yak/data_source.rb +1 -0
  46. data/test/files/yak/fringe_model.rb +4 -0
  47. data/test/files/yak/fringe_presenter.rb +4 -0
  48. data/test/files/yak/fringe_view.rb +1 -0
  49. data/test/files/yak/giant_squid.rb +4 -0
  50. data/test/files/yak/krill.rb +2 -0
  51. data/test/files/yak/my_objects.yml +21 -0
  52. data/test/files/yak/sub_sub_context_test.yml +27 -0
  53. data/test/test_helper.rb +38 -0
  54. metadata +118 -0
@@ -0,0 +1,3 @@
1
+ == 1.0.0 / 2007-11-19
2
+
3
+ * Released!
@@ -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
@@ -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.
@@ -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
@@ -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