conject 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/.gitignore +4 -0
  2. data/.rvmrc +2 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +32 -0
  5. data/NOTES.txt +61 -0
  6. data/README.md +8 -0
  7. data/Rakefile +12 -0
  8. data/TODO +9 -0
  9. data/conject.gemspec +21 -0
  10. data/lib/conject.rb +40 -0
  11. data/lib/conject/borrowed_active_support_inflector.rb +525 -0
  12. data/lib/conject/class_ext_construct_with.rb +123 -0
  13. data/lib/conject/class_finder.rb +11 -0
  14. data/lib/conject/composition_error.rb +33 -0
  15. data/lib/conject/dependency_resolver.rb +16 -0
  16. data/lib/conject/extended_metaid.rb +33 -0
  17. data/lib/conject/object_context.rb +61 -0
  18. data/lib/conject/object_definition.rb +10 -0
  19. data/lib/conject/object_factory.rb +28 -0
  20. data/lib/conject/utilities.rb +8 -0
  21. data/lib/conject/version.rb +3 -0
  22. data/rake_tasks/rspec.rake +25 -0
  23. data/spec/acceptance/dev/README +7 -0
  24. data/spec/acceptance/regression/README +12 -0
  25. data/spec/acceptance/regression/basic_composition_spec.rb +29 -0
  26. data/spec/acceptance/regression/basic_object_creation_spec.rb +42 -0
  27. data/spec/acceptance/regression/nested_contexts_spec.rb +86 -0
  28. data/spec/conject/borrowed_active_support_inflector_spec.rb +28 -0
  29. data/spec/conject/class_ext_construct_with_spec.rb +226 -0
  30. data/spec/conject/class_finder_spec.rb +36 -0
  31. data/spec/conject/composition_error_spec.rb +124 -0
  32. data/spec/conject/dependency_resolver_spec.rb +32 -0
  33. data/spec/conject/extended_metaid_spec.rb +90 -0
  34. data/spec/conject/object_context_spec.rb +186 -0
  35. data/spec/conject/object_definition_spec.rb +31 -0
  36. data/spec/conject/object_factory_spec.rb +89 -0
  37. data/spec/conject/utilities_spec.rb +30 -0
  38. data/spec/spec_helper.rb +24 -0
  39. data/spec/support/SPEC_HELPERS_GO_HERE +0 -0
  40. data/spec/support/load_path_helpers.rb +27 -0
  41. data/spec/test_data/basic_composition/fence.rb +5 -0
  42. data/spec/test_data/basic_composition/front_desk.rb +2 -0
  43. data/spec/test_data/basic_composition/grass.rb +2 -0
  44. data/spec/test_data/basic_composition/guest.rb +5 -0
  45. data/spec/test_data/basic_composition/lobby.rb +5 -0
  46. data/spec/test_data/basic_composition/nails.rb +2 -0
  47. data/spec/test_data/basic_composition/tv.rb +2 -0
  48. data/spec/test_data/basic_composition/wood.rb +2 -0
  49. data/spec/test_data/simple_stuff/some_random_class.rb +2 -0
  50. data/spike/arity_funny_business_in_different_ruby_versions.rb +34 -0
  51. data/spike/depends_on_spike.rb +146 -0
  52. data/spike/donkey_fail.rb +48 -0
  53. data/spike/donkey_journey.rb +50 -0
  54. data/spike/go.rb +11 -0
  55. data/spike/metaid.rb +28 -0
  56. data/spike/object_definition.rb +125 -0
  57. data/spike/sample.rb +125 -0
  58. data/src/user_model.rb +4 -0
  59. data/src/user_presenter.rb +10 -0
  60. data/src/user_view.rb +3 -0
  61. metadata +165 -0
@@ -0,0 +1,123 @@
1
+
2
+ class Class
3
+
4
+ def construct_with(*syms)
5
+ klass = self
6
+
7
+ object_def = Conject::ObjectDefinition.new(:owner => klass, :component_names => syms)
8
+ klass.meta_def :object_definition do
9
+ object_def
10
+ end
11
+
12
+
13
+ klass.class_def_private :components do
14
+ @_components ||= {}
15
+ end
16
+
17
+ syms.each do |object_name|
18
+ class_def_private object_name do
19
+ components[object_name]
20
+ end
21
+ end
22
+
23
+ klass.class_def_private :set_components do |component_map|
24
+ required = object_def.component_names
25
+ provided = component_map.keys
26
+ if required.to_set != provided.to_set
27
+ raise Conject::CompositionError.new(
28
+ :object_definition => object_def,
29
+ :required => required,
30
+ :provided => provided)
31
+ end
32
+
33
+ components.clear.merge! component_map
34
+
35
+ end
36
+
37
+ # Tidbits of state that our dynamically-defined functions herein
38
+ # will close over.
39
+ object_context_prep = {
40
+ :initialize_has_been_wrapped => false, # keep track of when a class's :initialize method has been wrapped
41
+ }
42
+
43
+ # Alias :new such that we can wrap and invoke it later
44
+ klass.meta_eval do
45
+ alias_method :actual_new, :new
46
+ end
47
+
48
+ # Override default :new behavior for this class.
49
+ #
50
+ # The .new method is rewritten to accept a single argument:
51
+ # component_map: a Hash containing all required objects to construct a new instance.
52
+ # Keys are expected to be symbols.
53
+ #
54
+ # If user defines their own #initialize method, all components sent into .new
55
+ # will be installed BEFORE the user-defined #initialize, and it may accept arguments thusly:
56
+ # * zero args. Nothing will be passed to #initialize
57
+ # * single arg. The component_map will be passed.
58
+ # * var args (eg, def initialize(*args)). args[0] will be the component map. NO OTHER ARGS WILL BE PASSED. See Footnote a)
59
+ #
60
+ klass.meta_def :new do |component_map|
61
+
62
+ # We only want to do the following one time, but we've waited until now
63
+ # in order to make sure our metaprogramming didn't get ahead of the user's
64
+ # own definition of initialize:
65
+ unless object_context_prep[:initialize_has_been_wrapped]
66
+ # Define a new wrapper'd version of initialize that accepts and uses a component map
67
+ alias_method :actual_initialize, :initialize
68
+ class_def :initialize do |component_map|
69
+ # Apply the given components
70
+ set_components component_map
71
+
72
+ # Invoke the normal initialize method.
73
+ # User-defined initialize method may accept 0 args, or it may accept a single arg
74
+ # which will be the component map.
75
+ arg_count = method(:actual_initialize).arity
76
+ case arg_count
77
+ when 0
78
+ actual_initialize
79
+ when 1, -1 # See Footnote a) at the bottom of this file
80
+
81
+ actual_initialize component_map
82
+
83
+ else
84
+ # We're not equipped to handle this
85
+ raise "User-defined initialize method defined with #{arg_count} parameters; must either be 0, other wise 1 or -1 (varargs) to receive the component map."
86
+ end
87
+ end
88
+ # Make a note that the initialize wrapper has been applied
89
+ object_context_prep[:initialize_has_been_wrapped] = true
90
+ end
91
+
92
+ # Instantiate an instance
93
+ actual_new component_map
94
+ end
95
+ end
96
+
97
+ def has_object_definition?
98
+ respond_to?(:object_definition) and !object_definition.nil?
99
+ end
100
+
101
+ end
102
+
103
+
104
+ # Footnote a) - Object#initialize arity funny business
105
+ #
106
+ # Before Ruby 1.9.3, Object#initialize has -1 arity by default and accepts 0 or
107
+ # more args. 1.9.3 changes Object#initialize to default to 0 args. This is
108
+ # deliberate (and in my opinion a long-overdue correction) but a mild problem
109
+ # -- I have no idea how I would determine in 1.9.2 or earlier if a user has
110
+ # defined NO constructor at all. However, passing component_map to an
111
+ # otherwise undefined initialize method won't hurt, so let's just do it.
112
+ #
113
+ # If in 1.9.3 a user really HAS defined a varargs #initialize, then let's
114
+ # expect that initialize method to conform to the same rules we're laying out
115
+ # for other cases: If you accept args at all, the first one will be the
116
+ # component_map. In this case NO ADDITIONAL ARGS WILL BE PASSED. (Who would send them in, anyway?)
117
+ # The idea here is that people using construct_with are not intending to construct
118
+ # new instances using some other input.
119
+ #
120
+ # Found this when struggling:
121
+ # http://bugs.ruby-lang.org/issues/5542 - Bug #5542: Ruby 1.9.3-p0 changed arity on default initialization method
122
+ #
123
+ # crosby 2012-01-07
@@ -0,0 +1,11 @@
1
+
2
+ class Conject::ClassFinder
3
+ def find_class(name)
4
+ cname = name.to_s.camelize
5
+ if Object.const_defined?(cname)
6
+ Object.const_get(cname)
7
+ else
8
+ raise "Could not find class for #{name}"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ require 'set'
2
+
3
+ class Conject::CompositionError < ArgumentError
4
+ def initialize(opts=nil)
5
+ opts ||= {}
6
+ object_def = opts[:object_definition]
7
+ required = nil
8
+ required = object_def.component_names if object_def
9
+ provided = opts[:provided] || []
10
+
11
+ msg = "Unexpected CompositionError"
12
+
13
+ if object_def.nil?
14
+ msg = "Failed to construct... something."
15
+ if provided and !provided.empty?
16
+ msg << " Provided objects were: #{provided.inspect}"
17
+ end
18
+
19
+ elsif object_def and required and provided
20
+ owner = object_def.owner || "object"
21
+ msg = "Wrong components when building new #{owner}."
22
+
23
+ missing = required - provided
24
+ msg << " Missing required object(s) #{missing.to_a.inspect}." unless missing.empty?
25
+
26
+ unexpected = provided - required
27
+ msg << " Unexpected object(s) provided #{unexpected.to_a.inspect}." unless unexpected.empty?
28
+
29
+ end
30
+
31
+ super msg
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ class Conject::DependencyResolver
2
+ #
3
+ # Given a Class, generate a map of dependencies needed to construct a new
4
+ # instance of that class. Dependencies are looked up (and/or instantiated, as
5
+ # determined within the ObjectContext) via the provided ObjectContext.
6
+ #
7
+ # This method assumes the Class has_object_defintion? (Client code should
8
+ # determine that before invoking this method.)
9
+ #
10
+ def resolve_for_class(klass, object_context)
11
+ klass.object_definition.component_names.inject({}) do |obj_map, name|
12
+ obj_map[name] = object_context.get(name)
13
+ obj_map
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ # Metaid == a few simple metaclass helper
2
+ # (See http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html.)
3
+ #
4
+ # 2012-01-03 crosby: Added module_def_private and class_def_private
5
+ #
6
+ class Object
7
+ # The hidden singleton lurks behind everyone
8
+ def metaclass; class << self; self; end; end
9
+ def meta_eval &blk; metaclass.instance_eval &blk; end
10
+
11
+ # Adds methods to a metaclass
12
+ def meta_def name, &blk
13
+ meta_eval { define_method name, &blk }
14
+ end
15
+ end
16
+
17
+ class Module
18
+ # Defines an instance method within a module
19
+ def module_def name, &blk
20
+ module_eval { define_method name, &blk }
21
+ end
22
+
23
+ # Same as module_def then makes the method private
24
+ def module_def_private name, &blk
25
+ module_def name, &blk
26
+ private name
27
+ end
28
+ end
29
+
30
+ class Class
31
+ alias class_def module_def
32
+ alias class_def_private module_def_private
33
+ end
@@ -0,0 +1,61 @@
1
+ class Conject::ObjectContext
2
+
3
+ construct_with :parent_context, :object_factory
4
+
5
+ def initialize
6
+ @cache = {}
7
+ end
8
+
9
+ # Inject a named object into this context
10
+ def put(name, object)
11
+ @cache[name.to_sym] = object
12
+ end
13
+
14
+ alias_method :[]=, :put
15
+
16
+ # Retrieve a named object from this context.
17
+ # If the object is already existant in this context, return it.
18
+ # If we have a parent context and it contains the requested object, get and return object from parent context. (Recursive upward search)
19
+ # If the object exists nowhere in this or a super context: construct, cache and return a new instance of the requested object using the object factory.
20
+ def get(name)
21
+ name = name.to_sym
22
+ object = @cache[name]
23
+ return @cache[name] if @cache.keys.include?(name)
24
+
25
+ if parent_context and parent_context.has?(name)
26
+ return parent_context.get(name)
27
+ else
28
+ object = object_factory.construct_new(name,self)
29
+ @cache[name] = object
30
+ return object
31
+ end
32
+ end
33
+
34
+ alias_method :[], :get
35
+
36
+ # Indicates if this context, or any parent context, contains the requested object in its cache.
37
+ def has?(name)
38
+ return true if directly_has?(name)
39
+
40
+ # Ask parent (if i have a parent) if I don't have the object:
41
+ if !parent_context.nil?
42
+ return parent_context.has?(name)
43
+ else
44
+ # I don't have it, and neither do my ancestors.
45
+ return false
46
+ end
47
+ end
48
+
49
+ # Indicates if this context has the requested object in its own personal cache.
50
+ # (Does not consult any parent contexts.)
51
+ def directly_has?(name)
52
+ @cache.keys.include?(name.to_sym)
53
+ end
54
+
55
+ def in_subcontext
56
+ yield Conject.create_object_context(self) if block_given?
57
+ end
58
+
59
+ end
60
+
61
+
@@ -0,0 +1,10 @@
1
+
2
+ class Conject::ObjectDefinition
3
+ attr_reader :component_names, :owner
4
+
5
+ def initialize(opts={})
6
+ @owner = opts[:owner]
7
+ @component_names = opts[:component_names] || []
8
+ end
9
+ end
10
+
@@ -0,0 +1,28 @@
1
+
2
+ class Conject::ObjectFactory
3
+ construct_with :class_finder, :dependency_resolver
4
+
5
+ def construct_new(name, object_context)
6
+
7
+ #
8
+ # This implementation is what I'm loosely calling "type 1" or "regular" object creation:
9
+ # - Assume we're looking for a class to create an instance with
10
+ # - it may or may not have a declared list of named objects it needs to be constructed with
11
+ #
12
+
13
+ klass = class_finder.find_class(name)
14
+
15
+ if klass.has_object_definition?
16
+ object_map = dependency_resolver.resolve_for_class(klass, object_context)
17
+ return klass.new(object_map)
18
+
19
+ elsif Conject::Utilities.has_zero_arg_constructor?(klass)
20
+ # Default construction
21
+ return klass.new
22
+ else
23
+ # Oops, out of ideas on how to build.
24
+ raise ArgumentError.new("Class #{klass} has no special component needs, but neither does it have a zero-argument constructor.");
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,8 @@
1
+ module Conject::Utilities
2
+ class << self
3
+ def has_zero_arg_constructor?(klass)
4
+ init_arity = klass.instance_method(:initialize).arity
5
+ init_arity == 0 or (RUBY_VERSION <= "1.9.2" and init_arity == -1)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ module Conject
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,25 @@
1
+ begin
2
+ require 'rspec/core/rake_task'
3
+ rescue Exception => e
4
+ "RSpec is not available. 'spec' task will not be defined."
5
+ end
6
+
7
+ begin # protect from missing rspec
8
+ desc "Run specs"
9
+ RSpec::Core::RakeTask.new do |t|
10
+ if ENV["file"]
11
+ t.pattern = ENV["file"]
12
+ end
13
+ t.rspec_opts = "--color --format documentation" # --tty
14
+ end
15
+
16
+ desc "Run individual spec"
17
+ task "spec:just" do
18
+ RSpec::Core::RakeTask.new("_tmp_rspec") do |t|
19
+ t.pattern = ENV["file"] || raise("Please supply 'file' argument")
20
+ t.rspec_opts = "--color"
21
+ end
22
+ Rake::Task["_tmp_rspec"].invoke
23
+ end
24
+ rescue Exception => e
25
+ end
@@ -0,0 +1,7 @@
1
+ Developing Acceptance Tests
2
+
3
+ This suite contains acceptance tests for features that are
4
+ UNDER DEVELOPMENT.
5
+
6
+ Once the features are complete, the test must be moved
7
+ into the "regression" peer directory.
@@ -0,0 +1,12 @@
1
+ Regression Acceptance Tests
2
+
3
+ This suite ensures that all defined features remain functional. Only
4
+ acceptance tests that are expected to ALWAYS PASS should live here.
5
+
6
+ Tests that are NOT FINISHED, or are failing because their targeted
7
+ features are UNDER DEVELOPMENT, go in the "dev" directory parallel to
8
+ this "regression" dir.
9
+
10
+ This applies to acceptance tests that are being redressed, or
11
+ whose target features are back under actual development.
12
+
@@ -0,0 +1,29 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+ describe "basic object composition" do
4
+ subject { Conject.default_object_context }
5
+
6
+ before do
7
+ append_test_load_path "basic_composition"
8
+ require 'fence'
9
+ require 'wood'
10
+ require 'nails'
11
+ end
12
+
13
+ after do
14
+ restore_load_path
15
+ end
16
+
17
+ it "constructs objects by providing necessary object components" do
18
+ fence = subject.get('fence')
19
+ fence.should_not be_nil
20
+
21
+ fence.wood.should_not be_nil
22
+ fence.wood.object_id.should == subject.get('wood').object_id
23
+
24
+ fence.nails.should_not be_nil
25
+ fence.nails.object_id.should == subject.get('nails').object_id
26
+ end
27
+
28
+ end
29
+
@@ -0,0 +1,42 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+
4
+ describe "simple object creation" do
5
+ subject { Conject.default_object_context }
6
+
7
+ before do
8
+ append_test_load_path "simple_stuff"
9
+ require 'some_random_class'
10
+ end
11
+
12
+ after do
13
+ restore_load_path
14
+ end
15
+
16
+ it "constructs simple objects" do
17
+ obj = subject.get('some_random_class')
18
+ obj.should_not be_nil
19
+ obj.class.should == SomeRandomClass
20
+ end
21
+
22
+ it "caches and returns references to same objects once built" do
23
+ obj = subject.get('some_random_class')
24
+ obj.should_not be_nil
25
+ obj.class.should == SomeRandomClass
26
+
27
+ obj2 = subject.get('some_random_class')
28
+ obj2.should_not be_nil
29
+ obj2.class.should == SomeRandomClass
30
+
31
+ obj.object_id.should == obj2.object_id
32
+ end
33
+
34
+ it "can use strings and symbols interchangeably for object keys" do
35
+ obj = subject.get('some_random_class')
36
+ obj.should_not be_nil
37
+ obj2 = subject.get(:some_random_class)
38
+ obj.object_id.should == obj2.object_id
39
+ end
40
+
41
+ end
42
+