abstracta-contracts 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec5dbea4ff031eb0fc61d786f5e5c443b0f9ecb6570e4f914531d4db4ae9e19d
4
- data.tar.gz: 5c03262e0801b1be7cec8f63ef3d9a8338ef363888abbbc95d135d0ec5df4155
3
+ metadata.gz: e53d4729e866578c1bf2c42041f2d31e6342014dd9e7630ed8b20a3a61d7ed7d
4
+ data.tar.gz: 5a41643d7443f7a2d615b85a0f356f4d29ea2d657c29bc0574a7aaff147a7d08
5
5
  SHA512:
6
- metadata.gz: 78c6dd7e8fe309f40d7e58a6485b01c3852a921c95064a9bb452ae834f3d81365bfe92f8abc254a17eedcef62993be46417ca6db26cf5d2f7b18cf8ea23eb727
7
- data.tar.gz: 0c2218ef59335cf982a665184427c103aba124d78a7ee8e14966ec50db413196273ac9c0712c42ad5db45636c3264c8d87b711545443084d3f47a81edff9083f
6
+ metadata.gz: ba6cacb1bf1693a2ef04cd0b21eec16c42493b4e5a4509f0afd4ac09845b7acb1bd0a8ee1be23beae3bda1110e22a8e1187350f715e97d91a89aa80a535d8463
7
+ data.tar.gz: 6873cef89d4ca90309737095debf5b0f03d0db8d3a02b7da2eb13423909558e98157f8894745d31dff54e790dd2276a5a1f53610b3e00f2deea9d2be63dc1c6b
data/CHANGELOG.md CHANGED
@@ -2,7 +2,21 @@
2
2
 
3
3
  All notable changes to AbstractaContracts will be documented in this file.
4
4
 
5
- ## [0.1.0] - Unreleased
5
+ ## Unreleased
6
+
7
+ ## [0.1.1] - 2026-09-08
8
+
9
+ ### Added
10
+
11
+ - Public YARD reference, generation task, and documentation check in CI.
12
+
13
+ ### Changed
14
+
15
+ - Group interface specs by definition, implementation, inheritance, and introspection.
16
+ - Make each spec explicitly load its helper for standalone execution.
17
+ - Correct the reusable-contract README example to preserve local variable scope.
18
+
19
+ ## [0.1.0] - 2026-09-04
6
20
 
7
21
  ### Added
8
22
 
data/README.md CHANGED
@@ -89,7 +89,7 @@ end
89
89
  ```ruby
90
90
  cache_contract = AbstractaContracts.with_methods(:read, :write, :delete)
91
91
 
92
- class RedisCache
92
+ RedisCache = Class.new do
93
93
  include cache_contract
94
94
 
95
95
  def read(key) = nil
@@ -186,6 +186,23 @@ RedisCache.missing_interface_class_methods
186
186
  - Interface requirements can be satisfied by the class, inherited implementations, or interface defaults.
187
187
  - AbstractaContracts has no runtime dependencies.
188
188
 
189
+ ## API reference
190
+
191
+ Run `bundle exec rake yard` and open `doc/index.html` for the public API.
192
+ The DSL entries on the AbstractaContracts page are methods installed on
193
+ consuming classes. Implementation details under `Internal` are unsupported.
194
+ Interface modules expose `interface?` (always true), `interface_methods`, and
195
+ `interface_class_methods` (frozen, deduplicated symbol arrays including parent
196
+ interfaces). Use `implements` to register an interface for validation; ordinary
197
+ Ruby inclusion alone does not register it as a class requirement.
198
+
199
+ Validation checks presence, not arity, argument types, or return values.
200
+ The constructor guard validates before forwarding arguments and blocks to
201
+ `new`; it does not prevent lower-level allocation or arbitrary Ruby overrides.
202
+ Rescue `AbstractaContracts::Error` for contract failures. Empty factory inputs
203
+ raise `ArgumentError`; including a factory result in the wrong kind of object
204
+ raises `TypeError`.
205
+
189
206
  ## Development
190
207
 
191
208
  ```bash
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AbstractaContracts
4
- VERSION = "0.1.0"
4
+ # Current gem version.
5
+ # @return [String]
6
+ VERSION = "0.1.1"
5
7
  end
@@ -7,15 +7,141 @@ require_relative "abstracta_contracts/internal/constructor_guard"
7
7
  require_relative "abstracta_contracts/internal/contract"
8
8
  require_relative "abstracta_contracts/internal/interface"
9
9
 
10
+ # Declarative abstract classes and reusable interfaces.
11
+ #
12
+ # Include this module in a class to install the class DSL documented below.
13
+ # The instance-method entries on this page describe methods installed on the
14
+ # consuming class, not methods on its instances or on AbstractaContracts itself.
15
+ # Contracts check method presence and ownership, not signatures or return types.
16
+ # Private and protected implementations count as present.
17
+ #
18
+ # @example Declare and implement a contract
19
+ # class Reader
20
+ # include AbstractaContracts.with_methods(:read)
21
+ # end
22
+ # class FileReader < Reader
23
+ # def read = :contents
24
+ # end
25
+ # FileReader.new.read # => :contents
10
26
  module AbstractaContracts
27
+ # @!method abstract_class!
28
+ # Mark only this class explicitly abstract; descendants do not inherit the marker.
29
+ # @return [Class] self
30
+ #
31
+ # @!method abstract_method(*names)
32
+ # Declare inherited instance requirements. Redeclaration requires a fresh implementation.
33
+ # @return [Class] self
34
+ # @param names [Array<String, Symbol, Array>] names to flatten and deduplicate
35
+ # @raise [AbstractaContracts::Error] for invalid method names
36
+ #
37
+ # @!method abstract_class_method(*names)
38
+ # Declare inherited class requirements. Redeclaration requires a fresh implementation.
39
+ # @return [Class] self
40
+ # @param names [Array<String, Symbol, Array>] names to flatten and deduplicate
41
+ # @raise [AbstractaContracts::Error] for invalid method names
42
+ #
43
+ # @!method implements(*interfaces)
44
+ # Register interfaces and include their modules. Repeated registrations are deduplicated.
45
+ # @return [Class] self
46
+ # @param interfaces [Array<Module, Array>] interface modules, optionally nested
47
+ # @raise [AbstractaContracts::Error] if any module is not an interface
48
+ #
49
+ # @!method explicitly_abstract?
50
+ # Whether this class itself was marked explicitly abstract.
51
+ # @return [Boolean]
52
+ #
53
+ # @!method abstract?
54
+ # Whether explicitly abstract or missing any abstract or interface requirement.
55
+ # @return [Boolean]
56
+ #
57
+ # @!method concrete?
58
+ # Whether this class can pass contract validation.
59
+ # @return [Boolean]
60
+ #
61
+ # @!method valid_implementation?
62
+ # Whether this class is concrete, including its explicit abstract marker.
63
+ # @return [Boolean]
64
+ #
65
+ # @!method validate_implementation!
66
+ # Validate all requirements and the explicit abstract marker without constructing an instance.
67
+ # @return [true] when concrete
68
+ # @raise [AbstractaContracts::Error] if methods are missing or the class is explicitly abstract
69
+ #
70
+ # @!method abstract_methods
71
+ # Required abstract instance names, including inherited declarations.
72
+ # @return [Array<Symbol>] frozen names
73
+ #
74
+ # @!method abstract_class_methods
75
+ # Required abstract class names, including inherited declarations.
76
+ # @return [Array<Symbol>] frozen names
77
+ #
78
+ # @!method direct_interfaces
79
+ # Interfaces registered directly on this class.
80
+ # @return [Array<Module>] frozen snapshot
81
+ #
82
+ # @!method interfaces
83
+ # Registered interfaces including class inheritance and interface ancestry.
84
+ # @return [Array<Module>] deduplicated frozen snapshot
85
+ #
86
+ # @!method implements?(interface)
87
+ # Whether an interface occurs in the complete registered interface hierarchy.
88
+ # @return [Boolean]
89
+ # @param interface [Module] an interface module
90
+ # @raise [AbstractaContracts::Error] if the argument is not an interface
91
+ #
92
+ # @!method interface_methods
93
+ # Required instance names from all registered interfaces.
94
+ # @return [Array<Symbol>] deduplicated frozen names
95
+ #
96
+ # @!method interface_class_methods
97
+ # Required class names from all registered interfaces.
98
+ # @return [Array<Symbol>] deduplicated frozen names
99
+ #
100
+ # @!method missing_abstract_methods
101
+ # Unresolved abstract instance requirements.
102
+ # @return [Array<Symbol>] frozen names
103
+ #
104
+ # @!method missing_abstract_class_methods
105
+ # Unresolved abstract class requirements.
106
+ # @return [Array<Symbol>] frozen names
107
+ #
108
+ # @!method missing_interface_methods
109
+ # Unresolved interface instance requirements.
110
+ # @return [Array<Symbol>] frozen names
111
+ #
112
+ # @!method missing_interface_class_methods
113
+ # Unresolved interface class requirements.
114
+ # @return [Array<Symbol>] frozen names
115
+ #
116
+ # @!method missing_methods
117
+ # Unresolved combined abstract and interface instance requirements.
118
+ # @return [Array<Symbol>] frozen names
119
+ #
120
+ # @!method missing_class_methods
121
+ # Unresolved combined abstract and interface class requirements.
122
+ # @return [Array<Symbol>] frozen names
123
+ #
124
+ # @private
11
125
  MUTEX_CREATION_LOCK = Mutex.new
12
126
  private_constant :MUTEX_CREATION_LOCK
13
127
 
14
128
  class << self
129
+ # @private
15
130
  def included(base)
16
131
  install(base)
17
132
  end
18
133
 
134
+ # Build a reusable abstract contract for inclusion in classes.
135
+ # Names are flattened, converted to symbols, and deduplicated. Requirements
136
+ # are inherited; redeclarations require an implementation at or below the
137
+ # new declaration in the method lookup chain.
138
+ # @param methods [Array<String, Symbol, Array>] instance method names
139
+ # @param class_methods [Array<String, Symbol>] class method names
140
+ # @return [Module] reusable contract; include it in each consuming class
141
+ # @raise [ArgumentError] if both lists are empty
142
+ # @raise [AbstractaContracts::Error] if a name is neither a string nor a symbol,
143
+ # is empty, or contains whitespace
144
+ # @raise [TypeError] when the returned contract is included in a module
19
145
  def with_methods(*methods, class_methods: [])
20
146
  instance_methods = normalize_method_names(methods)
21
147
  singleton_methods = normalize_method_names(Array(class_methods))
@@ -27,6 +153,24 @@ module AbstractaContracts
27
153
  Internal::Contract.new(instance_methods: instance_methods, class_methods: singleton_methods)
28
154
  end
29
155
 
156
+ # Build an interface definition for inclusion in a module.
157
+ # Include the result once in a module, then use the class DSL's implements
158
+ # method to register that module. Interfaces may include other interfaces
159
+ # and supply default instance methods. Inherited implementations also count.
160
+ # @param methods [Array<String, Symbol, Array>] instance method names
161
+ # @param class_methods [Array<String, Symbol>] class method names
162
+ # @return [Module] definition to include in an interface module
163
+ # @raise [ArgumentError] if both lists are empty
164
+ # @raise [AbstractaContracts::Error] for invalid names or repeated definition
165
+ # @raise [TypeError] when the definition is included in a class
166
+ # @example Define and implement an interface
167
+ # readable = Module.new { include AbstractaContracts.interface(:read) }
168
+ # reader = Class.new do
169
+ # include AbstractaContracts
170
+ # implements readable
171
+ # def read = :contents
172
+ # end
173
+ # reader.new.read # => :contents
30
174
  def interface(*methods, class_methods: [])
31
175
  instance_methods = normalize_method_names(methods)
32
176
  singleton_methods = normalize_method_names(Array(class_methods))
@@ -38,6 +182,7 @@ module AbstractaContracts
38
182
  Internal::Interface.new(instance_methods: instance_methods, class_methods: singleton_methods)
39
183
  end
40
184
 
185
+ # @private
41
186
  def install(base)
42
187
  raise TypeError, "AbstractaContracts can only be included in classes" unless base.is_a?(Class)
43
188
 
@@ -47,6 +192,7 @@ module AbstractaContracts
47
192
  base
48
193
  end
49
194
 
195
+ # @private
50
196
  def define_interface(base, instance_methods:, class_methods:)
51
197
  if base.instance_variable_defined?(:@abstracta_contracts_interface_defined)
52
198
  raise Internal::InterfaceAlreadyDefinedError,
@@ -60,20 +206,24 @@ module AbstractaContracts
60
206
  base
61
207
  end
62
208
 
209
+ # @private
63
210
  def interface?(object)
64
211
  object.is_a?(Module) && object.respond_to?(:interface?) && object.interface?
65
212
  end
66
213
 
214
+ # @private
67
215
  def validate_interface!(interface)
68
216
  return interface if interface?(interface)
69
217
 
70
218
  raise Internal::InvalidInterfaceError, "#{interface.inspect} is not an AbstractaContracts interface"
71
219
  end
72
220
 
221
+ # @private
73
222
  def normalize_interfaces(interfaces)
74
223
  interfaces.flatten.map { |interface| validate_interface!(interface) }.uniq.freeze
75
224
  end
76
225
 
226
+ # @private
77
227
  def normalize_method_names(names)
78
228
  names.flatten.map do |name|
79
229
  unless name.is_a?(String) || name.is_a?(Symbol)
@@ -89,6 +239,7 @@ module AbstractaContracts
89
239
  end.uniq.freeze
90
240
  end
91
241
 
242
+ # @private
92
243
  def register_instance_methods(base, names)
93
244
  install(base)
94
245
  synchronize(base) do
@@ -97,6 +248,7 @@ module AbstractaContracts
97
248
  end
98
249
  end
99
250
 
251
+ # @private
100
252
  def register_class_methods(base, names)
101
253
  install(base)
102
254
  synchronize(base) do
@@ -105,26 +257,31 @@ module AbstractaContracts
105
257
  end
106
258
  end
107
259
 
260
+ # @private
108
261
  def required_instance_methods_for(klass)
109
262
  required_methods_for(klass, :abstracta_contracts_declared_instance_methods)
110
263
  end
111
264
 
265
+ # @private
112
266
  def required_class_methods_for(klass)
113
267
  required_methods_for(klass, :abstracta_contracts_declared_class_methods)
114
268
  end
115
269
 
270
+ # @private
116
271
  def missing_instance_methods_for(klass)
117
272
  required_instance_methods_for(klass).filter_map do |name, declaration_owner|
118
273
  name unless implemented_after_declaration?(klass, name, declaration_owner, singleton: false)
119
274
  end
120
275
  end
121
276
 
277
+ # @private
122
278
  def missing_class_methods_for(klass)
123
279
  required_class_methods_for(klass).filter_map do |name, declaration_owner|
124
280
  name unless implemented_after_declaration?(klass, name, declaration_owner, singleton: true)
125
281
  end
126
282
  end
127
283
 
284
+ # @private
128
285
  def interfaces_for(klass)
129
286
  result = []
130
287
 
@@ -140,6 +297,7 @@ module AbstractaContracts
140
297
  result
141
298
  end
142
299
 
300
+ # @private
143
301
  def interface_instance_methods_for(interface)
144
302
  validate_interface!(interface)
145
303
  interface_hierarchy(interface).flat_map do |candidate|
@@ -147,6 +305,7 @@ module AbstractaContracts
147
305
  end.uniq.freeze
148
306
  end
149
307
 
308
+ # @private
150
309
  def interface_class_methods_for(interface)
151
310
  validate_interface!(interface)
152
311
  interface_hierarchy(interface).flat_map do |candidate|
@@ -154,40 +313,48 @@ module AbstractaContracts
154
313
  end.uniq.freeze
155
314
  end
156
315
 
316
+ # @private
157
317
  def required_interface_instance_methods_for(klass)
158
318
  interfaces_for(klass).flat_map { |interface| interface_instance_methods_for(interface) }.uniq
159
319
  end
160
320
 
321
+ # @private
161
322
  def required_interface_class_methods_for(klass)
162
323
  interfaces_for(klass).flat_map { |interface| interface_class_methods_for(interface) }.uniq
163
324
  end
164
325
 
326
+ # @private
165
327
  def missing_interface_instance_methods_for(klass)
166
328
  required_interface_instance_methods_for(klass).reject do |name|
167
329
  method_available?(klass, name, singleton: false)
168
330
  end
169
331
  end
170
332
 
333
+ # @private
171
334
  def missing_interface_class_methods_for(klass)
172
335
  required_interface_class_methods_for(klass).reject do |name|
173
336
  method_available?(klass, name, singleton: true)
174
337
  end
175
338
  end
176
339
 
340
+ # @private
177
341
  def synchronize(base, &)
178
342
  mutex_for(base).synchronize(&)
179
343
  end
180
344
 
345
+ # @private
181
346
  def class_name(klass)
182
347
  klass.name || klass.inspect
183
348
  end
184
349
 
350
+ # @private
185
351
  def interface_name(interface)
186
352
  interface.name || interface.inspect
187
353
  end
188
354
 
189
355
  private
190
356
 
357
+ # @private
191
358
  def mutex_for(base)
192
359
  if base.instance_variable_defined?(:@abstracta_contracts_mutex)
193
360
  return base.instance_variable_get(:@abstracta_contracts_mutex)
@@ -199,6 +366,7 @@ module AbstractaContracts
199
366
  end
200
367
  end
201
368
 
369
+ # @private
202
370
  def required_methods_for(klass, reader)
203
371
  declarations = {}
204
372
 
@@ -212,6 +380,7 @@ module AbstractaContracts
212
380
  declarations.freeze
213
381
  end
214
382
 
383
+ # @private
215
384
  def implemented_after_declaration?(klass, name, declaration_owner, singleton:)
216
385
  lookup_class = singleton ? klass.singleton_class : klass
217
386
  declaration_lookup_owner = singleton ? declaration_owner.singleton_class : declaration_owner
@@ -228,6 +397,7 @@ module AbstractaContracts
228
397
  false
229
398
  end
230
399
 
400
+ # @private
231
401
  def method_available?(klass, name, singleton:)
232
402
  lookup = singleton ? klass.singleton_class : klass
233
403
  lookup.instance_method(name)
@@ -236,6 +406,7 @@ module AbstractaContracts
236
406
  false
237
407
  end
238
408
 
409
+ # @private
239
410
  def interface_hierarchy(interface)
240
411
  validate_interface!(interface)
241
412
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abstracta-contracts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juan Furattini