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.
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