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