surrogate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1,7 @@
1
+ rvm use ruby-1.9.3-p125
2
+
3
+ root_dir="$(pwd)"
4
+
5
+ alias ac="rspec '$root_dir/spec/acceptance_spec.rb'"
6
+ alias sp="rspec $(find "$root_dir/spec" -name *_spec.rb | ruby -ne 'print unless /acceptance_spec/' | tr '\n' ' ')"
7
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cobbler.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/Readme.md ADDED
@@ -0,0 +1,54 @@
1
+ TODO
2
+ ----
3
+
4
+ * substitutability
5
+ * add methods for substitutability
6
+
7
+
8
+ Features for future vuersions
9
+ -----------------------------
10
+
11
+ * change queue notation from will_x_qeue(1,2,3) to will_x(1,2,3)
12
+ * arity option
13
+ * support for raising errors
14
+ * need some way to talk about and record blocks being passed
15
+ * support all rspec matchers (RSpec::Mocks::ArgumentMatchers)
16
+ * assertions for order of invocations & methods
17
+
18
+
19
+
20
+ Future subset substitutability
21
+
22
+ # ===== Substitutability =====
23
+
24
+ # real user is not a suitable substitute if missing methods that mock user has
25
+ user_class.should_not substitute_for Class.new
26
+
27
+ # real user must have all of mock user's methods to be substitutable
28
+ substitutable_real_user_class = Class.new do
29
+ def self.find() end
30
+ def initialize(id) end
31
+ def id() end
32
+ def name() end
33
+ def address() end
34
+ def phone_numbers() end
35
+ def add_phone_number(area_code, number) end
36
+ end
37
+ user_class.should substitute_for substitutable_real_user_class
38
+ user_class.should be_subset_of substitutable_real_user_class
39
+
40
+ # real user class is not a suitable substitutable if has extra methods, but is suitable subset
41
+ real_user_class = substitutable_real_user_class.clone
42
+ def real_user_class.some_class_meth() end
43
+ user_class.should_not substitute_for real_user_class
44
+ user_class.should be_subset_of real_user_class
45
+
46
+ real_user_class = substitutable_real_user_class.clone
47
+ real_user_class.send(:define_method, :some_instance_method) {}
48
+ user_class.should_not substitute_for real_user_class
49
+ user_class.should be_subset_of real_user_class
50
+
51
+ # subset substitutability does not work for superset
52
+ real_user_class = substitutable_real_user_class.clone
53
+ real_user_class.undef_method :address
54
+ user_class.should_not be_subset_of real_user_class
@@ -0,0 +1,126 @@
1
+ require 'set'
2
+
3
+ class Surrogate
4
+
5
+ # compares a surrogate to an object
6
+ class ApiComparer
7
+ attr_accessor :surrogate, :actual
8
+
9
+ def initialize(surrogate, actual)
10
+ self.surrogate, self.actual = surrogate, actual
11
+ end
12
+
13
+ def surrogate_methods
14
+ @surrogate_methods ||= SurrogateMethods.new(surrogate).methods
15
+ end
16
+
17
+ def actual_methods
18
+ @actual_methods ||= ActualMethods.new(actual).methods
19
+ end
20
+
21
+ def compare
22
+ @compare ||= {
23
+ instance: {
24
+ not_on_surrogate: instance_not_on_surrogate,
25
+ not_on_actual: instance_not_on_actual,
26
+ },
27
+ class: {
28
+ not_on_surrogate: class_not_on_surrogate,
29
+ not_on_actual: class_not_on_actual,
30
+ },
31
+ }
32
+ end
33
+
34
+ def instance_not_on_surrogate
35
+ (actual_methods[:instance][:inherited] + actual_methods[:instance][:other]) -
36
+ (surrogate_methods[:instance][:inherited] + surrogate_methods[:instance][:api])
37
+ end
38
+
39
+ def instance_not_on_actual
40
+ surrogate_methods[:instance][:api] - actual_methods[:instance][:inherited] - actual_methods[:instance][:other]
41
+ end
42
+
43
+ def class_not_on_surrogate
44
+ (actual_methods[:class][:inherited] + actual_methods[:class][:other]) -
45
+ (surrogate_methods[:class][:inherited] + surrogate_methods[:class][:api])
46
+ end
47
+
48
+ def class_not_on_actual
49
+ surrogate_methods[:class][:api] - actual_methods[:class][:inherited] - actual_methods[:class][:other]
50
+ end
51
+
52
+ # methods from the actual class (as opposed to "these are actually methods"
53
+ class ActualMethods < Struct.new(:actual)
54
+ def methods
55
+ { instance: {
56
+ inherited: instance_inherited_methods,
57
+ other: instance_other_methods,
58
+ },
59
+ class: {
60
+ inherited: class_inherited_methods,
61
+ other: class_other_methods,
62
+ },
63
+ }
64
+ end
65
+
66
+ def instance_inherited_methods
67
+ Set.new actual.instance_methods - actual.instance_methods(false)
68
+ end
69
+
70
+ def instance_other_methods
71
+ Set.new(actual.instance_methods) - instance_inherited_methods
72
+ end
73
+
74
+ def class_inherited_methods
75
+ Set.new actual.singleton_class.instance_methods - actual.singleton_class.instance_methods(false)
76
+ end
77
+
78
+ def class_other_methods
79
+ Set.new(actual.singleton_class.instance_methods) - class_inherited_methods
80
+ end
81
+ end
82
+
83
+
84
+ class SurrogateMethods < Struct.new(:surrogate)
85
+ def methods
86
+ { instance: {
87
+ api: instance_api_methods,
88
+ inherited: instance_inherited_methods,
89
+ other: instance_other_methods,
90
+ },
91
+ class: {
92
+ api: class_api_methods,
93
+ inherited: class_inherited_methods,
94
+ other: class_other_methods,
95
+ },
96
+ }
97
+ end
98
+
99
+ def instance_api_methods
100
+ Set.new surrogate.api_method_names
101
+ end
102
+
103
+ def instance_inherited_methods
104
+ Set.new surrogate.instance_methods - surrogate.instance_methods(false)
105
+ end
106
+
107
+ def instance_other_methods
108
+ Set.new(surrogate.instance_methods false) - instance_api_methods
109
+ end
110
+
111
+ def class_api_methods
112
+ Set.new surrogate.singleton_class.api_method_names
113
+ end
114
+
115
+ # should have new
116
+ def class_inherited_methods
117
+ Set.new surrogate.singleton_class.instance_methods - surrogate.singleton_class.instance_methods(false) + [:new]
118
+ end
119
+
120
+ # should not have new
121
+ def class_other_methods
122
+ Set.new(surrogate.singleton_class.instance_methods false) - class_api_methods - class_inherited_methods
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,108 @@
1
+ class Surrogate
2
+
3
+ # adds surrogate behaviour to your class / singleton class / instances
4
+ class Endower
5
+ def self.endow(klass, &playlist)
6
+ new(klass, &playlist).endow
7
+ end
8
+
9
+ attr_accessor :klass, :playlist
10
+
11
+ def initialize(klass, &playlist)
12
+ self.klass, self.playlist = klass, playlist
13
+ end
14
+
15
+ def endow
16
+ endow_klass
17
+ endow_singleton_class
18
+ end
19
+
20
+ private
21
+
22
+ def endow_klass
23
+ a_hatchery_for klass
24
+ enable_defining_methods klass
25
+ record_initialization_for_instances_of klass
26
+ remember_invocations_for_instances_of klass
27
+ remember_invocations_for_instances_of klass.singleton_class
28
+ hijack_instantiation_of klass
29
+ can_get_a_new klass
30
+ end
31
+
32
+ def endow_singleton_class
33
+ hatchery = a_hatchery_for singleton
34
+ enable_defining_methods singleton
35
+ singleton.module_eval &playlist if playlist
36
+ klass.instance_variable_set :@surrogate, Hatchling.new(klass, hatchery)
37
+ klass
38
+ end
39
+
40
+ # yeesh :( try to find a better way to do this
41
+ def record_initialization_for_instances_of(klass)
42
+ def klass.method_added(meth)
43
+ return unless meth == :initialize && !@hijacking_initialize
44
+ @hijacking_initialize = true
45
+ current_initialize = instance_method :initialize
46
+ define :initialize do |*args, &block|
47
+ current_initialize.bind(self).call(*args, &block)
48
+ end
49
+ ensure
50
+ @hijacking_initialize = false
51
+ end
52
+ initialize = klass.instance_method :initialize
53
+ klass.send :define_method, :initialize do |*args, &block|
54
+ initialize.bind(self).call(*args, &block)
55
+ end
56
+ end
57
+
58
+ def singleton
59
+ klass.singleton_class
60
+ end
61
+
62
+ def can_get_a_new(klass)
63
+ klass.define_singleton_method :reprise do
64
+ new_klass = Class.new self
65
+ surrogate = @surrogate
66
+ Surrogate.endow new_klass do
67
+ surrogate.api_methods.each do |method_name, options|
68
+ define method_name, options.to_hash, &options.default_proc
69
+ end
70
+ end
71
+ @hatchery.api_methods.each do |method_name, options|
72
+ new_klass.define method_name, options.to_hash, &options.default_proc
73
+ end
74
+ new_klass
75
+ end
76
+ end
77
+
78
+ def remember_invocations_for_instances_of(klass)
79
+ klass.send :define_method, :invocations do |method_name|
80
+ @surrogate.invocations method_name
81
+ end
82
+ end
83
+
84
+ def a_hatchery_for(klass)
85
+ klass.instance_variable_set :@hatchery, Surrogate::Hatchery.new(klass)
86
+ end
87
+
88
+ def hijack_instantiation_of(klass)
89
+ def klass.new(*args)
90
+ instance = allocate
91
+ hatchery = @hatchery
92
+ instance.instance_eval { @surrogate = Hatchling.new instance, hatchery }
93
+ instance.send :initialize, *args
94
+ instance
95
+ end
96
+ end
97
+
98
+ def enable_defining_methods(klass)
99
+ def klass.define(method_name, options={}, &block)
100
+ @hatchery.define method_name, options, block
101
+ end
102
+
103
+ def klass.api_method_names
104
+ @hatchery.api_method_names
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,48 @@
1
+ class Surrogate
2
+ class Hatchery
3
+ attr_accessor :klass
4
+
5
+ def initialize(klass)
6
+ self.klass = klass
7
+ defines_methods
8
+ end
9
+
10
+ def defines_methods
11
+ klass.singleton_class.send :define_method, :define, &method(:define)
12
+ end
13
+
14
+ def define(method_name, options={}, block)
15
+ add_api_methods_for method_name
16
+ api_methods[method_name] = Options.new options, block
17
+ end
18
+
19
+ def api_methods
20
+ @api_methods ||= {}
21
+ end
22
+
23
+ def api_method_names
24
+ api_methods.keys - [:initialize]
25
+ end
26
+
27
+ # here we need to find better domain terminology
28
+ def add_api_methods_for(method_name)
29
+ klass.send :define_method, method_name do |*args, &block|
30
+ @surrogate.invoke_method method_name, args, &block
31
+ end
32
+
33
+ # verbs
34
+ klass.send :define_method, "will_#{method_name}" do |*args, &block|
35
+ if args.size == 1
36
+ @surrogate.prepare_method method_name, args, &block
37
+ else
38
+ @surrogate.prepare_method_queue method_name, args, &block
39
+ end
40
+ self
41
+ end
42
+
43
+ # nouns
44
+ klass.send :alias_method, "will_have_#{method_name}", "will_#{method_name}"
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,78 @@
1
+ class Surrogate
2
+ UnknownMethod = Class.new StandardError
3
+ class Hatchling
4
+ attr_accessor :instance, :hatchery
5
+
6
+ def initialize(instance, hatchery)
7
+ self.instance, self.hatchery = instance, hatchery
8
+ end
9
+
10
+ def api_methods
11
+ hatchery.api_methods
12
+ end
13
+
14
+ def invoke_method(method_name, args, &block)
15
+ invoked_methods[method_name] << args
16
+ return get_default method_name, args unless has_ivar? method_name
17
+ ivar = get_ivar method_name
18
+ return ivar unless ivar.kind_of? MethodQueue
19
+ play_from_queue ivar, method_name
20
+ end
21
+
22
+ def prepare_method(method_name, args, &block)
23
+ set_ivar method_name, *args
24
+ end
25
+
26
+ def prepare_method_queue(method_name, args, &block)
27
+ set_ivar method_name, MethodQueue.new(args)
28
+ end
29
+
30
+ def invocations(method_name)
31
+ invoked_methods[method_name]
32
+ end
33
+
34
+ private
35
+
36
+ def invoked_methods
37
+ @invoked_methods ||= Hash.new do |hash, method_name|
38
+ must_know method_name
39
+ hash[method_name] = []
40
+ end
41
+ end
42
+
43
+ def get_default(method_name, args)
44
+ api_methods[method_name].default instance, args do
45
+ raise UnpreparedMethodError, "#{method_name} has been invoked without being told how to behave"
46
+ end
47
+ end
48
+
49
+ def play_from_queue(queue, method_name)
50
+ result = queue.dequeue
51
+ unset_ivar method_name if queue.empty?
52
+ result
53
+ end
54
+
55
+ def must_know(method_name)
56
+ return if api_methods.has_key? method_name
57
+ known_methods = api_methods.keys.map(&:to_s).map(&:inspect).join ', '
58
+ raise UnknownMethod, "doesn't know \"#{method_name}\", only knows #{known_methods}"
59
+ end
60
+
61
+ def has_ivar?(method_name)
62
+ instance.instance_variable_defined? "@#{method_name}"
63
+ end
64
+
65
+ def set_ivar(method_name, value)
66
+ instance.instance_variable_set "@#{method_name}", value
67
+ end
68
+
69
+ def get_ivar(method_name)
70
+ instance.instance_variable_get "@#{method_name}"
71
+ end
72
+
73
+ def unset_ivar(method_name)
74
+ instance.send :remove_instance_variable, "@#{method_name}"
75
+ end
76
+ end
77
+ end
78
+
@@ -0,0 +1,14 @@
1
+ class Surrogate
2
+ class MethodQueue < Struct.new(:queue)
3
+ QueueEmpty = Class.new StandardError
4
+
5
+ def dequeue
6
+ raise QueueEmpty if empty?
7
+ queue.shift
8
+ end
9
+
10
+ def empty?
11
+ queue.empty?
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ class Surrogate
2
+ class Options
3
+ attr_accessor :options, :default_proc
4
+
5
+ def initialize(options, default_proc)
6
+ self.options, self.default_proc = options, default_proc
7
+ end
8
+
9
+ def has?(name)
10
+ options.has_key? name
11
+ end
12
+
13
+ def [](key)
14
+ options[key]
15
+ end
16
+
17
+ def to_hash
18
+ options
19
+ end
20
+
21
+ def default(instance, args, &no_default)
22
+ return options[:default] if options.has_key? :default
23
+ return instance.instance_exec(*args, &default_proc) if default_proc
24
+ no_default.call
25
+ end
26
+ end
27
+ end
28
+