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
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'simplecov' # SimpleCov must come first!
|
2
|
+
# This start/config code could alternatively go in .simplecov in project root:
|
3
|
+
SimpleCov.start do
|
4
|
+
add_filter "/spec/"
|
5
|
+
add_filter "/borrowed_active_support_inflector.rb" # We stole it precious, our specs only hit about 47% of the functions, just to prove we have this library ready to go
|
6
|
+
end
|
7
|
+
|
8
|
+
PROJECT_ROOT = File.expand_path(File.dirname(__FILE__) + "/..")
|
9
|
+
|
10
|
+
$LOAD_PATH << "#{PROJECT_ROOT}/lib"
|
11
|
+
$LOAD_PATH << "#{PROJECT_ROOT}/spec/samples"
|
12
|
+
ENV["APP_ENV"] = "rspec"
|
13
|
+
|
14
|
+
require 'conject'
|
15
|
+
|
16
|
+
# Load all support files
|
17
|
+
Dir["#{PROJECT_ROOT}/spec/support/*.rb"].each do |support|
|
18
|
+
require support
|
19
|
+
end
|
20
|
+
|
21
|
+
RSpec.configure do |config|
|
22
|
+
config.include LoadPathHelpers
|
23
|
+
end
|
24
|
+
|
File without changes
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
module LoadPathHelpers
|
3
|
+
def test_data_path(rel_path='')
|
4
|
+
"#{PROJECT_ROOT}/spec/test_data/#{rel_path}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def append_load_path(path)
|
8
|
+
@appended_load_paths ||= []
|
9
|
+
@appended_load_paths << path
|
10
|
+
$LOAD_PATH << path
|
11
|
+
end
|
12
|
+
|
13
|
+
def append_test_load_path(path)
|
14
|
+
append_load_path test_data_path(path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def restore_load_path
|
18
|
+
(@appended_load_paths || []).each do |path|
|
19
|
+
remove_load_path path
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove_load_path(path)
|
24
|
+
$LOAD_PATH.delete path
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
$: << "../lib"
|
2
|
+
require 'object_context'
|
3
|
+
require 'object_context/utilities'
|
4
|
+
|
5
|
+
class A
|
6
|
+
end
|
7
|
+
|
8
|
+
class B
|
9
|
+
def initialize
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class C
|
14
|
+
def initialize(*x)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class D
|
19
|
+
def initialize(x)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
#
|
25
|
+
# RUBY 1.9.2 says Object#initialize is arity -1
|
26
|
+
# RUBY 1.9.3 (and 1.8.7??) say it's 0 (as expected)
|
27
|
+
# This discrepancy is being handled by ObjectContext::Utilities.has_zero_arg_constructor?
|
28
|
+
#
|
29
|
+
puts "RUBY_PLATFORM: #{RUBY_PLATFORM}"
|
30
|
+
puts "RUBY_VERSION: #{RUBY_VERSION}"
|
31
|
+
[A,B,C,D].each do |c|
|
32
|
+
puts "Class #{c}#initialize arity: #{c.instance_method(:initialize).arity}"
|
33
|
+
puts "Class #{c} has_zero_arg_constructor?: #{ObjectContext::Utilities.has_zero_arg_constructor?(c)}"
|
34
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
$: << "."
|
2
|
+
require 'metaid'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
class ObjectDefinition
|
6
|
+
attr_reader :component_names, :owner, :construction_type, :singleton
|
7
|
+
|
8
|
+
def initialize(opts={})
|
9
|
+
@owner = opts[:owner]
|
10
|
+
@component_names = opts[:component_names] || []
|
11
|
+
@construction_type = :instance
|
12
|
+
@singleton = true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
class Class
|
18
|
+
|
19
|
+
def depends_on(*syms)
|
20
|
+
#puts "Class #{name} wishes to be constructed with #{syms.inspect}"
|
21
|
+
|
22
|
+
klass = self
|
23
|
+
|
24
|
+
object_def = ObjectDefinition.new(:owner => klass, :component_names => syms)
|
25
|
+
klass.meta_def :object_definition do
|
26
|
+
object_def
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
klass.class_def_private :components do
|
31
|
+
@_components ||= {}
|
32
|
+
end
|
33
|
+
|
34
|
+
syms.each do |object_name|
|
35
|
+
class_def_private object_name do
|
36
|
+
components[object_name]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
klass.class_def_private :set_components do |component_map|
|
41
|
+
required = object_def.component_names.to_set
|
42
|
+
provided = component_map.keys.to_set
|
43
|
+
if required != provided
|
44
|
+
msg = "Wrong components when building new #{object_def.owner}:"
|
45
|
+
|
46
|
+
missing = required - provided
|
47
|
+
msg << "Required objects not provided: #{missing.to_a.inspect}" unless missing.empty?
|
48
|
+
|
49
|
+
unexpected = provided - required
|
50
|
+
msg << "Unexpected objects: #{unexpected.to_a.inspect}" unless unexpected.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
components.clear.merge! component_map
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
# Tidbits of state that our dynamically-defined functions herein
|
58
|
+
# will close over.
|
59
|
+
object_context_prep = {
|
60
|
+
:initialize_has_been_wrapped => false, # keep track of when a class's :initialize method has been wrapped
|
61
|
+
}
|
62
|
+
# klass.meta_def :object_context_prep do
|
63
|
+
# @_object_context_prep ||= {
|
64
|
+
# :initialize_has_been_wrapped => false
|
65
|
+
# }
|
66
|
+
# end
|
67
|
+
|
68
|
+
# Alias :new such that we can wrap and invoke it later
|
69
|
+
klass.meta_eval do
|
70
|
+
alias_method :actual_new, :new
|
71
|
+
end
|
72
|
+
|
73
|
+
# Override default :new behavior for this class:
|
74
|
+
klass.meta_def :new do |component_map|
|
75
|
+
|
76
|
+
# We only want to do the following one time, but we've waited until now
|
77
|
+
# in order to make sure our metaprogramming didn't get ahead of the user's
|
78
|
+
# own definition of initialize:
|
79
|
+
unless object_context_prep[:initialize_has_been_wrapped]
|
80
|
+
puts " (wrapping :initialize, should only happen once)"
|
81
|
+
|
82
|
+
# Define a new wrapper'd version of initialize that accepts and uses a component map
|
83
|
+
alias_method :actual_initialize, :initialize
|
84
|
+
class_def :initialize do |component_map|
|
85
|
+
# Apply the given components
|
86
|
+
set_components component_map
|
87
|
+
|
88
|
+
# Invoke the normal initialize method.
|
89
|
+
# User-defined initialize method may accept 0 args, or it may accept a single arg
|
90
|
+
# which will be the component map.
|
91
|
+
arg_count = method(:actual_initialize).arity
|
92
|
+
case arg_count
|
93
|
+
when 0
|
94
|
+
actual_initialize
|
95
|
+
when 1
|
96
|
+
actual_initialize component_map
|
97
|
+
else
|
98
|
+
# We're not equipped to handle this
|
99
|
+
raise "User-defined initialize method defined with #{arg_count} paramters; must either be 0, or 1 to receive the component map."
|
100
|
+
end
|
101
|
+
end
|
102
|
+
# Make a note that the initialize wrapper has been applied
|
103
|
+
object_context_prep[:initialize_has_been_wrapped] = true
|
104
|
+
end
|
105
|
+
|
106
|
+
# Instantiate an instance
|
107
|
+
actual_new component_map
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def has_object_definition?
|
112
|
+
respond_to?(:object_definition) and !object_definition.nil?
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
class Car
|
118
|
+
depends_on :doors
|
119
|
+
|
120
|
+
def initialize(cm)
|
121
|
+
puts "User defined initialize: Construcing a new Car #{cm}. Can talk about doors=#{doors}"
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
class Doors
|
127
|
+
def initialize
|
128
|
+
puts "Doors constructed the old fashioned way"
|
129
|
+
$stdout.flush
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
c = Car.new(:doors => "whatever")
|
134
|
+
$stdout.flush
|
135
|
+
d = Doors.new
|
136
|
+
c2 = Car.new(:doors => "more whatever")
|
137
|
+
p c
|
138
|
+
p d
|
139
|
+
p c2
|
140
|
+
#p c2.components # will fail, components should be private
|
141
|
+
puts "Doors has an object def? #{Doors.has_object_definition?}"
|
142
|
+
puts "Car has an object def? #{Car.has_object_definition?}"
|
143
|
+
p Car.object_definition
|
144
|
+
puts "Car class requires components: #{Car.object_definition.component_names}"
|
145
|
+
puts "OK! (If this line prints out then the basic premise is working, though we're not really doing anything to assign components yet)"
|
146
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
$: << "."
|
2
|
+
require 'object_definition'
|
3
|
+
|
4
|
+
class Donkey
|
5
|
+
depends_on :head, :legs
|
6
|
+
|
7
|
+
def journey
|
8
|
+
head.talk
|
9
|
+
legs.walk
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Head
|
14
|
+
depends_on :eyes, :mouth
|
15
|
+
|
16
|
+
def talk
|
17
|
+
mouth.bray
|
18
|
+
eyes.roll
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Legs
|
23
|
+
def walk
|
24
|
+
puts "Clop clop!"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Eyes
|
29
|
+
def roll
|
30
|
+
puts "(eye roll)"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Mouth
|
35
|
+
def bray
|
36
|
+
puts "HAWWW!!"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
donkey = Donkey.new(
|
41
|
+
:head => Head.new(
|
42
|
+
:one => "dude",
|
43
|
+
:two => "dudes",
|
44
|
+
),
|
45
|
+
:legs => Legs.new,
|
46
|
+
)
|
47
|
+
|
48
|
+
donkey.journey
|
@@ -0,0 +1,50 @@
|
|
1
|
+
$: << "."
|
2
|
+
require 'object_definition'
|
3
|
+
|
4
|
+
class Donkey
|
5
|
+
depends_on :head, :legs
|
6
|
+
|
7
|
+
def journey
|
8
|
+
head.talk
|
9
|
+
legs.walk
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
class Head
|
15
|
+
depends_on :eyes, :mouth
|
16
|
+
|
17
|
+
def talk
|
18
|
+
mouth.bray
|
19
|
+
eyes.roll
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Legs
|
24
|
+
def walk
|
25
|
+
puts "Clop clop!"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Eyes
|
30
|
+
def roll
|
31
|
+
puts "(eye roll)"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Mouth
|
36
|
+
def bray
|
37
|
+
puts "HAWWW!!"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
donkey = Donkey.new(
|
42
|
+
:head => Head.new(
|
43
|
+
:mouth => Mouth.new,
|
44
|
+
:eyes => Eyes.new
|
45
|
+
),
|
46
|
+
:legs => Legs.new
|
47
|
+
)
|
48
|
+
|
49
|
+
donkey.journey
|
50
|
+
|
data/spike/go.rb
ADDED
data/spike/metaid.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Metaid == a few simple metaclass helper
|
2
|
+
# (See http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html.)
|
3
|
+
class Object
|
4
|
+
# The hidden singleton lurks behind everyone
|
5
|
+
def metaclass; class << self; self; end; end
|
6
|
+
def meta_eval &blk; metaclass.instance_eval &blk; end
|
7
|
+
|
8
|
+
# Adds methods to a metaclass
|
9
|
+
def meta_def name, &blk
|
10
|
+
meta_eval { define_method name, &blk }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Module
|
15
|
+
# Defines an instance method within a module
|
16
|
+
def module_def name, &blk
|
17
|
+
module_eval { define_method name, &blk }
|
18
|
+
end
|
19
|
+
def module_def_private name, &blk
|
20
|
+
module_def name, &blk
|
21
|
+
private name
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Class
|
26
|
+
alias class_def module_def
|
27
|
+
alias class_def_private module_def_private
|
28
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'metaid'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
class ObjectDefinition
|
5
|
+
attr_reader :component_names, :owner, :construction_type, :singleton
|
6
|
+
|
7
|
+
def initialize(opts={})
|
8
|
+
@owner = opts[:owner]
|
9
|
+
@component_names = opts[:component_names] || []
|
10
|
+
@construction_type = :instance
|
11
|
+
@singleton = true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class CompositionError < RuntimeError
|
16
|
+
def initialize(opts={})
|
17
|
+
object_def = opts[:object_definition]
|
18
|
+
required = opts[:required]
|
19
|
+
provided = opts[:provided]
|
20
|
+
|
21
|
+
msg = "Wrong components when building new #{object_def.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 << "Encountered unexpected object(s) #{unexpected.to_a.inspect}. " unless unexpected.empty?
|
28
|
+
|
29
|
+
super msg
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Class
|
34
|
+
|
35
|
+
def depends_on(*syms)
|
36
|
+
klass = self
|
37
|
+
|
38
|
+
object_def = ObjectDefinition.new(:owner => klass, :component_names => syms)
|
39
|
+
klass.meta_def :object_definition do
|
40
|
+
object_def
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
klass.class_def_private :components do
|
45
|
+
@_components ||= {}
|
46
|
+
end
|
47
|
+
|
48
|
+
syms.each do |object_name|
|
49
|
+
class_def_private object_name do
|
50
|
+
components[object_name]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
klass.class_def_private :set_components do |component_map|
|
55
|
+
required = object_def.component_names.to_set
|
56
|
+
provided = component_map.keys.to_set
|
57
|
+
if required != provided
|
58
|
+
raise CompositionError.new(
|
59
|
+
:object_definition => object_def,
|
60
|
+
:required => required,
|
61
|
+
:provided => provided)
|
62
|
+
end
|
63
|
+
|
64
|
+
components.clear.merge! component_map
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
# Tidbits of state that our dynamically-defined functions herein
|
69
|
+
# will close over.
|
70
|
+
object_context_prep = {
|
71
|
+
:initialize_has_been_wrapped => false, # keep track of when a class's :initialize method has been wrapped
|
72
|
+
}
|
73
|
+
# klass.meta_def :object_context_prep do
|
74
|
+
# @_object_context_prep ||= {
|
75
|
+
# :initialize_has_been_wrapped => false
|
76
|
+
# }
|
77
|
+
# end
|
78
|
+
|
79
|
+
# Alias :new such that we can wrap and invoke it later
|
80
|
+
klass.meta_eval do
|
81
|
+
alias_method :actual_new, :new
|
82
|
+
end
|
83
|
+
|
84
|
+
# Override default :new behavior for this class:
|
85
|
+
klass.meta_def :new do |component_map|
|
86
|
+
|
87
|
+
# We only want to do the following one time, but we've waited until now
|
88
|
+
# in order to make sure our metaprogramming didn't get ahead of the user's
|
89
|
+
# own definition of initialize:
|
90
|
+
unless object_context_prep[:initialize_has_been_wrapped]
|
91
|
+
# Define a new wrapper'd version of initialize that accepts and uses a component map
|
92
|
+
alias_method :actual_initialize, :initialize
|
93
|
+
class_def :initialize do |component_map|
|
94
|
+
# Apply the given components
|
95
|
+
set_components component_map
|
96
|
+
|
97
|
+
# Invoke the normal initialize method.
|
98
|
+
# User-defined initialize method may accept 0 args, or it may accept a single arg
|
99
|
+
# which will be the component map.
|
100
|
+
arg_count = method(:actual_initialize).arity
|
101
|
+
case arg_count
|
102
|
+
when 0
|
103
|
+
actual_initialize
|
104
|
+
when 1
|
105
|
+
actual_initialize component_map
|
106
|
+
else
|
107
|
+
# We're not equipped to handle this
|
108
|
+
raise "User-defined initialize method defined with #{arg_count} paramters; must either be 0, or 1 to receive the component map."
|
109
|
+
end
|
110
|
+
end
|
111
|
+
# Make a note that the initialize wrapper has been applied
|
112
|
+
object_context_prep[:initialize_has_been_wrapped] = true
|
113
|
+
end
|
114
|
+
|
115
|
+
# Instantiate an instance
|
116
|
+
actual_new component_map
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def has_object_definition?
|
121
|
+
respond_to?(:object_definition) and !object_definition.nil?
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|