surrogate 0.1.0

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