conject 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rvmrc +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +32 -0
- data/NOTES.txt +61 -0
- data/README.md +8 -0
- data/Rakefile +12 -0
- data/TODO +9 -0
- data/conject.gemspec +21 -0
- data/lib/conject.rb +40 -0
- data/lib/conject/borrowed_active_support_inflector.rb +525 -0
- data/lib/conject/class_ext_construct_with.rb +123 -0
- data/lib/conject/class_finder.rb +11 -0
- data/lib/conject/composition_error.rb +33 -0
- data/lib/conject/dependency_resolver.rb +16 -0
- data/lib/conject/extended_metaid.rb +33 -0
- data/lib/conject/object_context.rb +61 -0
- data/lib/conject/object_definition.rb +10 -0
- data/lib/conject/object_factory.rb +28 -0
- data/lib/conject/utilities.rb +8 -0
- data/lib/conject/version.rb +3 -0
- data/rake_tasks/rspec.rake +25 -0
- data/spec/acceptance/dev/README +7 -0
- data/spec/acceptance/regression/README +12 -0
- data/spec/acceptance/regression/basic_composition_spec.rb +29 -0
- data/spec/acceptance/regression/basic_object_creation_spec.rb +42 -0
- data/spec/acceptance/regression/nested_contexts_spec.rb +86 -0
- data/spec/conject/borrowed_active_support_inflector_spec.rb +28 -0
- data/spec/conject/class_ext_construct_with_spec.rb +226 -0
- data/spec/conject/class_finder_spec.rb +36 -0
- data/spec/conject/composition_error_spec.rb +124 -0
- data/spec/conject/dependency_resolver_spec.rb +32 -0
- data/spec/conject/extended_metaid_spec.rb +90 -0
- data/spec/conject/object_context_spec.rb +186 -0
- data/spec/conject/object_definition_spec.rb +31 -0
- data/spec/conject/object_factory_spec.rb +89 -0
- data/spec/conject/utilities_spec.rb +30 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/SPEC_HELPERS_GO_HERE +0 -0
- data/spec/support/load_path_helpers.rb +27 -0
- data/spec/test_data/basic_composition/fence.rb +5 -0
- data/spec/test_data/basic_composition/front_desk.rb +2 -0
- data/spec/test_data/basic_composition/grass.rb +2 -0
- data/spec/test_data/basic_composition/guest.rb +5 -0
- data/spec/test_data/basic_composition/lobby.rb +5 -0
- data/spec/test_data/basic_composition/nails.rb +2 -0
- data/spec/test_data/basic_composition/tv.rb +2 -0
- data/spec/test_data/basic_composition/wood.rb +2 -0
- data/spec/test_data/simple_stuff/some_random_class.rb +2 -0
- data/spike/arity_funny_business_in_different_ruby_versions.rb +34 -0
- data/spike/depends_on_spike.rb +146 -0
- data/spike/donkey_fail.rb +48 -0
- data/spike/donkey_journey.rb +50 -0
- data/spike/go.rb +11 -0
- data/spike/metaid.rb +28 -0
- data/spike/object_definition.rb +125 -0
- data/spike/sample.rb +125 -0
- data/src/user_model.rb +4 -0
- data/src/user_presenter.rb +10 -0
- data/src/user_view.rb +3 -0
- 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,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,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,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,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
|
+
|