conject 0.0.1

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 (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
+