interface 1.0.5 → 1.2.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/lib/interface.rb CHANGED
@@ -1,99 +1,312 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # A module for implementing Java style interfaces in Ruby. For more information
2
4
  # about Java interfaces, please see:
3
5
  #
4
6
  # http://java.sun.com/docs/books/tutorial/java/concepts/interface.html
5
7
  #
8
+ # @author Daniel J. Berger
9
+ # @since 1.0.0
6
10
  module Interface
7
11
  # The version of the interface library.
8
- Interface::VERSION = '1.0.5'.freeze
12
+ VERSION = '1.2.0'.freeze
9
13
 
10
14
  # Raised if a class or instance does not meet the interface requirements.
11
- class MethodMissing < RuntimeError; end
15
+ # Provides detailed information about which methods are missing and from which target.
16
+ class MethodMissing < RuntimeError
17
+ # @return [Array<Symbol>] the missing method names
18
+ attr_reader :missing_methods
19
+
20
+ # @return [String] the name of the target class/module
21
+ attr_reader :target_name
22
+
23
+ # @return [String] the name of the interface
24
+ attr_reader :interface_name
12
25
 
13
- alias :extends :extend
26
+ # Creates a new MethodMissing error with detailed information
27
+ #
28
+ # @param missing_methods [Array<Symbol>, Symbol] the missing method name(s)
29
+ # @param target [Module, Class] the target class or module
30
+ # @param interface_mod [Module] the interface module
31
+ def initialize(missing_methods, target = nil, interface_mod = nil)
32
+ @missing_methods = Array(missing_methods)
33
+ @target_name = target&.name || target&.class&.name || 'Unknown'
34
+ @interface_name = interface_mod&.name || 'Unknown Interface'
35
+
36
+ methods_list = @missing_methods.map { |m| "`#{m}`" }.join(', ')
37
+ super("#{@target_name} must implement #{methods_list} to satisfy #{@interface_name}")
38
+ end
39
+ end
40
+
41
+ alias extends extend
14
42
 
15
43
  private
16
44
 
45
+ # Handles extending an object with the interface
46
+ #
47
+ # @param obj [Object] the object to extend
48
+ # @return [Object] the extended object
17
49
  def extend_object(obj)
18
- return append_features(obj) if Interface === obj
19
- append_features(class << obj; self end)
50
+ return append_features(obj) if obj.is_a?(Interface)
51
+ append_features(obj.singleton_class)
20
52
  included(obj)
21
53
  end
22
54
 
55
+ # Validates interface requirements when included/extended
56
+ #
57
+ # @param mod [Module] the module being extended
58
+ # @return [Module] the module
59
+ # @raise [Interface::MethodMissing] if required methods are not implemented
23
60
  def append_features(mod)
24
- return super if Interface === mod
61
+ return super if mod.is_a?(Interface)
25
62
 
26
- # Is this a sub-interface?
27
- inherited = (self.ancestors-[self]).select{ |x| Interface === x }
28
- inherited = inherited.map{ |x| x.instance_variable_get('@ids') }
63
+ # For extend on instances or immediate validation
64
+ if should_validate_immediately?(mod)
65
+ validate_interface_requirements(mod)
66
+ end
67
+ super
68
+ end
29
69
 
30
- # Store required method ids
31
- ids = @ids + inherited.flatten
32
- @unreq ||= []
70
+ # Called when this interface is included in a class or module
71
+ #
72
+ # @param base [Class, Module] the class or module that included this interface
73
+ def included(base)
74
+ super
75
+ return if base.is_a?(Interface)
76
+
77
+ interface_module = self
78
+
79
+ # For classes, set up method tracking to validate after all methods are defined
80
+ if base.is_a?(Class)
81
+ # Store reference to interface for later validation
82
+ base.instance_variable_set(:@pending_interface_validations,
83
+ (base.instance_variable_get(:@pending_interface_validations) || []) + [interface_module])
84
+
85
+ # Set up method_added callback if not already done
86
+ unless base.respond_to?(:interface_method_added_original)
87
+ base.singleton_class.alias_method(:interface_method_added_original, :method_added) if base.respond_to?(:method_added)
88
+
89
+ base.define_singleton_method(:method_added) do |method_name|
90
+ # Call original method_added if it existed
91
+ interface_method_added_original(method_name) if respond_to?(:interface_method_added_original)
33
92
 
34
- # Iterate over the methods, minus the unrequired methods, and raise
35
- # an error if the method has not been defined.
36
- (ids - @unreq).uniq.each do |id|
37
- unless mod.instance_methods(true).include?(id)
38
- raise Interface::MethodMissing, id
93
+ # Check if all pending interfaces are now satisfied
94
+ pending = instance_variable_get(:@pending_interface_validations) || []
95
+ pending.each do |interface_mod|
96
+ if interface_mod.satisfied_by?(self)
97
+ # Interface is satisfied, remove from pending
98
+ pending.delete(interface_mod)
99
+ end
100
+ end
101
+ instance_variable_set(:@pending_interface_validations, pending)
102
+ end
103
+
104
+ # Set up validation at class end using TracePoint
105
+ setup_deferred_validation(base)
39
106
  end
107
+ else
108
+ # For modules and instances, validate immediately
109
+ validate_interface_requirements(base)
110
+ end
111
+ end
112
+
113
+ # Determines if we should validate immediately or defer validation
114
+ #
115
+ # @param mod [Module] the module to check
116
+ # @return [Boolean] true if validation should happen immediately
117
+ def should_validate_immediately?(mod)
118
+ required_method_ids = compute_required_methods
119
+ return true if required_method_ids.empty?
120
+
121
+ # Always validate immediately for instances (singleton classes)
122
+ return true if mod.singleton_class?
123
+
124
+ # Check if any required methods are already defined
125
+ implemented_methods = get_implemented_methods(mod)
126
+ (required_method_ids & implemented_methods).any?
127
+ rescue NoMethodError
128
+ # If instance_methods fails, this is likely an instance, validate immediately
129
+ true
130
+ end
131
+
132
+ # Sets up deferred validation using TracePoint to detect when class definition ends
133
+ #
134
+ # @param base [Class, Module] the class or module to validate later
135
+ def setup_deferred_validation(base)
136
+ interface_module = self
137
+
138
+ # Use TracePoint to detect when we're done defining the class
139
+ trace = TracePoint.new(:end) do |tp|
140
+ # Check if we're ending the definition of our target class
141
+ if tp.self == base
142
+ trace.disable
143
+
144
+ # Validate any remaining pending interfaces
145
+ pending = base.instance_variable_get(:@pending_interface_validations) || []
146
+ pending.each do |interface_mod|
147
+ begin
148
+ interface_mod.send(:validate_interface_requirements, base)
149
+ rescue => e
150
+ raise e
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ trace.enable
157
+ end
158
+
159
+ # Validates that all required methods are implemented
160
+ #
161
+ # @param mod [Module] the module to validate
162
+ # @raise [Interface::MethodMissing] if required methods are missing
163
+ def validate_interface_requirements(mod)
164
+ required_method_ids = compute_required_methods
165
+ implemented_methods = get_implemented_methods(mod)
166
+ missing_methods = required_method_ids - implemented_methods
167
+
168
+ return if missing_methods.empty?
169
+
170
+ # For backward compatibility, raise with first missing method if only one
171
+ # Otherwise use the enhanced error with full details
172
+ if missing_methods.size == 1
173
+ raise Interface::MethodMissing.new(missing_methods.first, mod, self)
174
+ else
175
+ raise Interface::MethodMissing.new(missing_methods, mod, self)
40
176
  end
177
+ end
41
178
 
42
- super mod
179
+ # Computes the final list of required methods after inheritance and unrequired methods
180
+ #
181
+ # @return [Array<Symbol>] the required method symbols
182
+ def compute_required_methods
183
+ inherited_methods = compute_inherited_methods
184
+ all_required = ((@ids || []) + inherited_methods).uniq
185
+ all_required - (@unreq || [])
186
+ end
187
+
188
+ # Gets inherited method requirements from parent interfaces
189
+ #
190
+ # @return [Array<Symbol>] inherited required methods
191
+ def compute_inherited_methods
192
+ parent_interfaces = ancestors.drop(1).select { |ancestor| ancestor.is_a?(Interface) }
193
+ parent_interfaces.flat_map { |interface| interface.instance_variable_get(:@ids) || [] }
194
+ end
195
+
196
+ # Gets the list of implemented methods for a module
197
+ #
198
+ # @param mod [Module] the module to check
199
+ # @return [Array<Symbol>] implemented method names
200
+ def get_implemented_methods(mod)
201
+ if mod.respond_to?(:instance_methods)
202
+ mod.instance_methods(true)
203
+ else
204
+ # For instances, get methods from their singleton class
205
+ mod.methods.map(&:to_sym)
206
+ end
43
207
  end
44
208
 
45
209
  public
46
210
 
47
- # Accepts an array of method names that define the interface. When this
211
+ # Accepts an array of method names that define the interface. When this
48
212
  # module is included/implemented, those method names must have already been
49
213
  # defined.
50
214
  #
51
- def required_methods(*ids)
52
- @ids = ids
215
+ # @param method_names [Array<Symbol>] method names that must be implemented
216
+ # @return [Array<Symbol>] the updated list of required methods
217
+ # @raise [ArgumentError] if no method names are provided
218
+ # @example
219
+ # MyInterface = interface do
220
+ # required_methods :foo, :bar, :baz
221
+ # end
222
+ def required_methods(*method_names)
223
+ raise ArgumentError, 'At least one method name must be provided' if method_names.empty?
224
+
225
+ @ids = method_names.map(&:to_sym)
53
226
  end
54
227
 
55
228
  # Accepts an array of method names that are removed as a requirement for
56
229
  # implementation. Presumably you would use this in a sub-interface where
57
230
  # you only wanted a partial implementation of an existing interface.
58
231
  #
59
- def unrequired_methods(*ids)
232
+ # @param method_names [Array<Symbol>] method names to remove from requirements
233
+ # @return [Array<Symbol>] the updated list of unrequired methods
234
+ # @example
235
+ # SubInterface = interface do
236
+ # extends ParentInterface
237
+ # unrequired_methods :optional_method
238
+ # end
239
+ def unrequired_methods(*method_names)
60
240
  @unreq ||= []
61
- @unreq += ids
241
+ return @unreq if method_names.empty?
242
+
243
+ @unreq += method_names.map(&:to_sym)
244
+ end
245
+
246
+ # Returns the list of all required methods for this interface
247
+ #
248
+ # @return [Array<Symbol>] all required method names
249
+ def get_required_methods
250
+ compute_required_methods
251
+ end
252
+
253
+ # Returns the list of unrequired methods for this interface
254
+ #
255
+ # @return [Array<Symbol>] unrequired method names
256
+ def get_unrequired_methods
257
+ (@unreq || []).dup
258
+ end
259
+
260
+ # Checks if a class or module implements this interface
261
+ #
262
+ # @param target [Class, Module] the class or module to check
263
+ # @return [Boolean] true if the interface is satisfied
264
+ def satisfied_by?(target)
265
+ required_method_ids = compute_required_methods
266
+ implemented_methods = get_implemented_methods(target)
267
+ (required_method_ids - implemented_methods).empty?
62
268
  end
63
269
  end
64
270
 
271
+ # Extends Object to provide the interface method for creating interfaces
65
272
  class Object
66
273
  # The interface method creates an interface module which typically sets
67
274
  # a list of methods that must be defined in the including class or module.
68
275
  # If the methods are not defined, an Interface::MethodMissing error is raised.
69
276
  #
70
- # A interface can extend an existing interface as well. These are called
71
- # sub-interfaces, and they can included the rules for their parent interface
277
+ # An interface can extend an existing interface as well. These are called
278
+ # sub-interfaces, and they can include the rules for their parent interface
72
279
  # by simply extending it.
73
280
  #
74
- # Example:
281
+ # @yield [Module] the interface module for configuration
282
+ # @return [Module] a new interface module
283
+ # @raise [ArgumentError] if no block is provided
75
284
  #
285
+ # @example Basic interface
76
286
  # # Require 'alpha' and 'beta' methods
77
- # AlphaInterface = interface{
78
- # required_methods :alpha, :beta
79
- # }
287
+ # AlphaInterface = interface do
288
+ # required_methods :alpha, :beta
289
+ # end
80
290
  #
291
+ # @example Sub-interface
81
292
  # # A sub-interface that requires 'beta' and 'gamma' only
82
- # GammaInterface = interface{
83
- # extends AlphaInterface
84
- # required_methods :gamma
85
- # unrequired_methods :alpha
86
- # }
293
+ # GammaInterface = interface do
294
+ # extends AlphaInterface
295
+ # required_methods :gamma
296
+ # unrequired_methods :alpha
297
+ # end
87
298
  #
299
+ # @example Usage with error
88
300
  # # Raises an Interface::MethodMissing error because :beta is not defined.
89
301
  # class MyClass
90
- # def alpha
91
- # # ...
92
- # end
93
- # implements AlphaInterface
302
+ # implements AlphaInterface
303
+ # def alpha
304
+ # # ...
305
+ # end
94
306
  # end
95
- #
96
307
  def interface(&block)
308
+ raise ArgumentError, 'Block required for interface definition' unless block_given?
309
+
97
310
  Module.new do |mod|
98
311
  mod.extend(Interface)
99
312
  mod.instance_eval(&block)
@@ -101,6 +314,21 @@ class Object
101
314
  end
102
315
  end
103
316
 
317
+ # Extends Module to provide the implements method as an alias for include
104
318
  class Module
105
- alias :implements :include
319
+ # Implements an interface by including it. This is syntactic sugar
320
+ # that makes the intent clearer when working with interfaces.
321
+ #
322
+ # @param interface_modules [Array<Module>] one or more interface modules
323
+ # @return [self]
324
+ #
325
+ # @example
326
+ # class MyClass
327
+ # implements MyInterface
328
+ #
329
+ # def required_method
330
+ # # implementation
331
+ # end
332
+ # end
333
+ alias implements include
106
334
  end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require 'interface'
5
+
6
+ RSpec.describe 'Interface Enhanced Features' do
7
+ describe 'enhanced error messages' do
8
+ it 'provides detailed error information for single missing method' do
9
+ basic_interface = interface do
10
+ required_methods :method_a, :method_b
11
+ end
12
+
13
+ # Use a block to ensure the error happens synchronously
14
+ expect do
15
+ Class.new do
16
+ def method_a; end
17
+ # missing method_b
18
+ include basic_interface
19
+ end
20
+ end.to raise_error(Interface::MethodMissing) do |error|
21
+ expect(error.missing_methods).to include(:method_b)
22
+ expect(error.message).to include('must implement')
23
+ end
24
+ end
25
+
26
+ it 'supports include at top of class definition' do
27
+ basic_interface = interface do
28
+ required_methods :method_a, :method_b
29
+ end
30
+
31
+ # Test the main functionality - include at top with working methods
32
+ test_class = Class.new do
33
+ include basic_interface
34
+ def method_a; 'a'; end
35
+ def method_b; 'b'; end
36
+ end
37
+
38
+ expect(test_class.new.method_a).to eq('a')
39
+ expect(test_class.new.method_b).to eq('b')
40
+ end
41
+ end
42
+
43
+ describe 'interface validation methods' do
44
+ it 'returns required methods list' do
45
+ basic_interface = interface do
46
+ required_methods :method_a, :method_b
47
+ end
48
+
49
+ expect(basic_interface.get_required_methods).to contain_exactly(:method_a, :method_b)
50
+ end
51
+
52
+ it 'returns unrequired methods list for sub-interfaces' do
53
+ basic_interface = interface do
54
+ required_methods :method_a, :method_b
55
+ end
56
+
57
+ extended_interface = Module.new do
58
+ extend Interface
59
+ extend basic_interface
60
+ required_methods :method_c
61
+ unrequired_methods :method_a
62
+ end
63
+
64
+ expect(extended_interface.get_unrequired_methods).to contain_exactly(:method_a)
65
+ end
66
+
67
+ it 'correctly computes final required methods for sub-interfaces' do
68
+ basic_interface = interface do
69
+ required_methods :method_a, :method_b
70
+ end
71
+
72
+ extended_interface = Module.new do
73
+ extend Interface
74
+ extend basic_interface
75
+ required_methods :method_c
76
+ unrequired_methods :method_a
77
+ end
78
+
79
+ final_required = extended_interface.get_required_methods
80
+ expect(final_required).to contain_exactly(:method_b, :method_c)
81
+ expect(final_required).not_to include(:method_a)
82
+ end
83
+
84
+ describe 'satisfied_by? method' do
85
+ let(:implementing_class) do
86
+ Class.new do
87
+ def method_a; 'a'; end
88
+ def method_b; 'b'; end
89
+ def method_c; 'c'; end
90
+ end
91
+ end
92
+
93
+ it 'returns true when interface is satisfied' do
94
+ basic_interface = interface do
95
+ required_methods :method_a, :method_b
96
+ end
97
+
98
+ expect(basic_interface.satisfied_by?(implementing_class)).to be true
99
+
100
+ extended_interface = Module.new do
101
+ extend Interface
102
+ extend basic_interface
103
+ required_methods :method_c
104
+ unrequired_methods :method_a
105
+ end
106
+
107
+ expect(extended_interface.satisfied_by?(implementing_class)).to be true
108
+ end
109
+
110
+ it 'returns false when interface is not satisfied' do
111
+ basic_interface = interface do
112
+ required_methods :method_a, :method_b
113
+ end
114
+
115
+ incomplete_class = Class.new do
116
+ def method_a; 'a'; end
117
+ # missing method_b
118
+ end
119
+
120
+ expect(basic_interface.satisfied_by?(incomplete_class)).to be false
121
+ end
122
+ end
123
+ end
124
+
125
+ describe 'parameter validation' do
126
+ it 'raises error when no methods provided to required_methods' do
127
+ expect do
128
+ interface do
129
+ required_methods
130
+ end
131
+ end.to raise_error(ArgumentError, 'At least one method name must be provided')
132
+ end
133
+
134
+ it 'raises error when no block provided to interface' do
135
+ expect do
136
+ Object.new.interface
137
+ end.to raise_error(ArgumentError, 'Block required for interface definition')
138
+ end
139
+ end
140
+
141
+ describe 'symbol conversion' do
142
+ it 'converts string method names to symbols' do
143
+ string_interface = interface do
144
+ required_methods 'string_method', 'another_method'
145
+ end
146
+
147
+ expect(string_interface.get_required_methods).to contain_exactly(:string_method, :another_method)
148
+ end
149
+ end
150
+
151
+ describe 'complex inheritance scenarios' do
152
+ it 'handles multiple levels of inheritance correctly' do
153
+ base_interface = interface do
154
+ required_methods :base_method
155
+ end
156
+
157
+ middle_interface = Module.new do
158
+ extend Interface
159
+ extend base_interface
160
+ required_methods :middle_method
161
+ end
162
+
163
+ final_interface = Module.new do
164
+ extend Interface
165
+ extend middle_interface
166
+ required_methods :final_method
167
+ unrequired_methods :base_method
168
+ end
169
+
170
+ required = final_interface.get_required_methods
171
+ expect(required).to contain_exactly(:middle_method, :final_method)
172
+ expect(required).not_to include(:base_method)
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,56 @@
1
+ #####################################################
2
+ # interface_spec.rb
3
+ #
4
+ # Test suite for the Interface module.
5
+ #####################################################
6
+ require 'rspec'
7
+ require 'interface'
8
+
9
+ RSpec.describe 'Interface' do
10
+ alpha_interface = interface{ required_methods :alpha, :beta }
11
+
12
+ gamma_interface = interface{
13
+ extends alpha_interface
14
+ required_methods :gamma
15
+ unrequired_methods :alpha
16
+ }
17
+
18
+ let(:class_a){ Class.new }
19
+
20
+ let(:class_b){
21
+ Class.new do
22
+ def alpha; end
23
+ def beta; end
24
+ end
25
+ }
26
+
27
+ let(:class_c){
28
+ Class.new do
29
+ def beta; end
30
+ def gamma; end
31
+ end
32
+ }
33
+
34
+ example "version" do
35
+ expect(Interface::VERSION).to eq('1.2.0')
36
+ expect(Interface::VERSION).to be_frozen
37
+ end
38
+
39
+ example "interface_requirements_not_met" do
40
+ expect{ class_a.extend(alpha_interface) }.to raise_error(Interface::MethodMissing)
41
+ expect{ class_a.new.extend(alpha_interface) }.to raise_error(Interface::MethodMissing)
42
+ end
43
+
44
+ example "sub_interface_requirements_not_met" do
45
+ expect{ class_b.extend(gamma_interface) }.to raise_error(Interface::MethodMissing)
46
+ expect{ class_b.new.extend(gamma_interface) }.to raise_error(Interface::MethodMissing)
47
+ end
48
+
49
+ example "alpha_interface_requirements_met" do
50
+ expect{ class_b.new.extend(alpha_interface) }.not_to raise_error
51
+ end
52
+
53
+ example "gamma_interface_requirements_met" do
54
+ expect{ class_c.new.extend(gamma_interface) }.not_to raise_error
55
+ end
56
+ end
data.tar.gz.sig CHANGED
Binary file