abstracta-contracts 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec5dbea4ff031eb0fc61d786f5e5c443b0f9ecb6570e4f914531d4db4ae9e19d
4
+ data.tar.gz: 5c03262e0801b1be7cec8f63ef3d9a8338ef363888abbbc95d135d0ec5df4155
5
+ SHA512:
6
+ metadata.gz: 78c6dd7e8fe309f40d7e58a6485b01c3852a921c95064a9bb452ae834f3d81365bfe92f8abc254a17eedcef62993be46417ca6db26cf5d2f7b18cf8ea23eb727
7
+ data.tar.gz: 0c2218ef59335cf982a665184427c103aba124d78a7ee8e14966ec50db413196273ac9c0712c42ad5db45636c3264c8d87b711545443084d3f47a81edff9083f
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to AbstractaContracts will be documented in this file.
4
+
5
+ ## [0.1.0] - Unreleased
6
+
7
+ ### Added
8
+
9
+ - Declarative abstract classes with `AbstractaContracts.with_methods`.
10
+ - Explicit abstract class, instance-method, and class-method contracts.
11
+ - Reusable interfaces with `AbstractaContracts.interface` and `implements`.
12
+ - Inherited and composable contracts across class and interface hierarchies.
13
+ - Runtime instantiation validation and contract introspection.
14
+ - Private implementation namespace under `AbstractaContracts::Internal`.
15
+ - RSpec, RuboCop, branch-aware SimpleCov thresholds, CI matrix, Dependabot, and Trusted Publishing release verification.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Juan Furattini
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # Abstracta Contracts
2
+
3
+ ## Public API
4
+
5
+ AbstractaContracts exposes one supported entry point:
6
+
7
+ ```ruby
8
+ require "abstracta_contracts"
9
+ ```
10
+
11
+ The supported root API is intentionally small:
12
+
13
+ - `AbstractaContracts.with_methods`
14
+ - `AbstractaContracts.interface`
15
+ - `AbstractaContracts::Error`
16
+ - `AbstractaContracts::VERSION`
17
+ - the class DSL installed by `include AbstractaContracts`: `abstract_class!`, `abstract_method`, `abstract_class_method`, and `implements`
18
+ - the documented introspection methods installed by `include AbstractaContracts`
19
+
20
+ Implementation modules, contract objects, guards, and specialized errors live behind `AbstractaContracts::Internal` and are not public API. No pre-release compatibility entry points are shipped.
21
+
22
+
23
+ Declarative abstract classes and interfaces for Ruby.
24
+
25
+ > **Gem:** `abstracta-contracts`
26
+ > **Namespace:** `AbstractaContracts`
27
+
28
+ ## Installation
29
+
30
+ ```ruby
31
+ gem "abstracta-contracts", require: "abstracta_contracts"
32
+ ```
33
+
34
+ ```ruby
35
+ require "abstracta_contracts"
36
+ ```
37
+
38
+ ## Abstract classes
39
+
40
+ The compact API is the recommended form:
41
+
42
+ ```ruby
43
+ class FeatureProvider
44
+ include AbstractaContracts.with_methods(:enabled?, :features)
45
+ end
46
+ ```
47
+
48
+ A class with an incomplete contract cannot be instantiated. A descendant becomes concrete when it implements every required method.
49
+
50
+ ```ruby
51
+ class RedisProvider < FeatureProvider
52
+ def enabled?(feature)
53
+ features.include?(feature)
54
+ end
55
+
56
+ def features
57
+ [:search, :reports]
58
+ end
59
+ end
60
+ ```
61
+
62
+ Class-method contracts use `class_methods:`:
63
+
64
+ ```ruby
65
+ class Provider
66
+ include AbstractaContracts.with_methods(:call, class_methods: [:provider_name])
67
+ end
68
+ ```
69
+
70
+ For dynamic or incremental declarations, use the explicit DSL:
71
+
72
+ ```ruby
73
+ class Provider
74
+ include AbstractaContracts
75
+
76
+ abstract_class!
77
+ abstract_method :enabled?
78
+ abstract_method :features
79
+ abstract_class_method :provider_name
80
+ end
81
+ ```
82
+
83
+ `abstract_class!` marks only the declaring class as explicitly abstract. The marker is not inherited. Method requirements are inherited and can be extended or redeclared by descendants.
84
+
85
+ ## Reusable abstract contracts
86
+
87
+ `with_methods` returns a reusable module-like contract:
88
+
89
+ ```ruby
90
+ cache_contract = AbstractaContracts.with_methods(:read, :write, :delete)
91
+
92
+ class RedisCache
93
+ include cache_contract
94
+
95
+ def read(key) = nil
96
+ def write(key, value) = value
97
+ def delete(key) = nil
98
+ end
99
+ ```
100
+
101
+ ## Interfaces
102
+
103
+ Interfaces are separate from abstract classes:
104
+
105
+ ```ruby
106
+ module Cacheable
107
+ include AbstractaContracts.interface(
108
+ :read,
109
+ :write,
110
+ :delete,
111
+ class_methods: [:adapter_name]
112
+ )
113
+ end
114
+ ```
115
+
116
+ Classes opt into AbstractaContracts and explicitly declare interfaces:
117
+
118
+ ```ruby
119
+ class RedisCache
120
+ include AbstractaContracts
121
+ implements Cacheable
122
+
123
+ def read(key) = nil
124
+ def write(key, value) = value
125
+ def delete(key) = nil
126
+ def self.adapter_name = :redis
127
+ end
128
+ ```
129
+
130
+ AbstractaContracts deliberately does not add `implements` to every Ruby class.
131
+
132
+ ### Interface inheritance and defaults
133
+
134
+ An interface may include another AbstractaContracts interface. Interface modules may also provide default instance methods; those methods satisfy their requirements.
135
+
136
+ ```ruby
137
+ module Readable
138
+ include AbstractaContracts.interface(:read)
139
+ end
140
+
141
+ module Cacheable
142
+ include Readable
143
+ include AbstractaContracts.interface(:write)
144
+
145
+ def read(key) = nil
146
+ end
147
+ ```
148
+
149
+ ## Introspection
150
+
151
+ Abstract classes expose:
152
+
153
+ ```ruby
154
+ Provider.abstract?
155
+ Provider.concrete?
156
+ Provider.explicitly_abstract?
157
+ Provider.abstract_methods
158
+ Provider.abstract_class_methods
159
+ Provider.missing_abstract_methods
160
+ Provider.missing_abstract_class_methods
161
+ Provider.valid_implementation?
162
+ Provider.validate_implementation!
163
+ ```
164
+
165
+ Classes implementing interfaces also expose:
166
+
167
+ ```ruby
168
+ RedisCache.interfaces
169
+ RedisCache.direct_interfaces
170
+ RedisCache.implements?(Cacheable)
171
+ RedisCache.interface_methods
172
+ RedisCache.interface_class_methods
173
+ RedisCache.missing_interface_methods
174
+ RedisCache.missing_interface_class_methods
175
+ ```
176
+
177
+ `missing_methods` and `missing_class_methods` return the combined unresolved abstract-class and interface requirements.
178
+
179
+ ## Contract rules
180
+
181
+ - Abstract method contracts accumulate through inheritance.
182
+ - Redeclaring a method as abstract requires a fresh implementation below that declaration.
183
+ - Private and protected methods can satisfy contracts.
184
+ - Modules included below an abstract declaration can satisfy instance-method contracts.
185
+ - Interfaces remain distinct from abstract classes.
186
+ - Interface requirements can be satisfied by the class, inherited implementations, or interface defaults.
187
+ - AbstractaContracts has no runtime dependencies.
188
+
189
+ ## Development
190
+
191
+ ```bash
192
+ bundle install
193
+ bundle exec rake
194
+ bundle exec gem build abstracta-contracts.gemspec
195
+ ```
196
+
197
+ ## Release
198
+
199
+ The repository includes CI and a RubyGems Trusted Publishing workflow. Repository synchronization, branches, commits, tags, and other version-control operations are intentionally outside the gem's responsibilities.
200
+
201
+ ## License
202
+
203
+ MIT.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractaContracts
4
+ # Public base exception for all AbstractaContracts errors.
5
+ class Error < StandardError; end
6
+
7
+ module Internal
8
+ class InvalidMethodNameError < Error; end
9
+ class InvalidInterfaceError < Error; end
10
+ class InterfaceAlreadyDefinedError < Error; end
11
+
12
+ class AbstractClassInstantiationError < Error
13
+ attr_reader :abstract_class
14
+
15
+ def initialize(abstract_class, message: nil)
16
+ @abstract_class = abstract_class
17
+ super(message || "#{AbstractaContracts.class_name(abstract_class)} is abstract and cannot be instantiated")
18
+ end
19
+ end
20
+
21
+ class UnimplementedMethodsError < AbstractClassInstantiationError
22
+ attr_reader :missing_instance_methods, :missing_class_methods
23
+
24
+ def initialize(abstract_class, instance_methods:, class_methods:)
25
+ @missing_instance_methods = instance_methods.freeze
26
+ @missing_class_methods = class_methods.freeze
27
+ super(abstract_class, message: unimplemented_message(abstract_class, instance_methods, class_methods))
28
+ end
29
+
30
+ private
31
+
32
+ def unimplemented_message(abstract_class, instance_methods, class_methods)
33
+ details = []
34
+ unless instance_methods.empty?
35
+ details << "instance methods: #{instance_methods.map { |name| "##{name}" }.join(', ')}"
36
+ end
37
+ details << "class methods: #{class_methods.map { |name| ".#{name}" }.join(', ')}" unless class_methods.empty?
38
+
39
+ "#{AbstractaContracts.class_name(abstract_class)} has unimplemented contract #{details.join('; ')}"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractaContracts
4
+ module Internal
5
+ module ClassMethods
6
+ def abstract_class!
7
+ AbstractaContracts.synchronize(self) { @abstracta_contracts_explicitly_abstract = true }
8
+ self
9
+ end
10
+
11
+ def abstract_method(*names)
12
+ AbstractaContracts.register_instance_methods(self, AbstractaContracts.normalize_method_names(names))
13
+ self
14
+ end
15
+
16
+ def abstract_class_method(*names)
17
+ AbstractaContracts.register_class_methods(self, AbstractaContracts.normalize_method_names(names))
18
+ self
19
+ end
20
+
21
+ def implements(*interfaces)
22
+ normalized = AbstractaContracts.normalize_interfaces(interfaces)
23
+
24
+ AbstractaContracts.synchronize(self) do
25
+ direct = abstracta_contracts_direct_interfaces
26
+ normalized.each { |interface| direct << interface unless direct.include?(interface) }
27
+ end
28
+
29
+ normalized.each { |interface| include(interface) unless self < interface }
30
+ self
31
+ end
32
+
33
+ def explicitly_abstract?
34
+ @abstracta_contracts_explicitly_abstract == true
35
+ end
36
+
37
+ def abstract?
38
+ explicitly_abstract? || !missing_methods.empty? || !missing_class_methods.empty?
39
+ end
40
+
41
+ def concrete?
42
+ !abstract?
43
+ end
44
+
45
+ def abstract_methods
46
+ AbstractaContracts.required_instance_methods_for(self).keys.freeze
47
+ end
48
+
49
+ def abstract_class_methods
50
+ AbstractaContracts.required_class_methods_for(self).keys.freeze
51
+ end
52
+
53
+ def missing_abstract_methods
54
+ AbstractaContracts.missing_instance_methods_for(self).freeze
55
+ end
56
+
57
+ def missing_abstract_class_methods
58
+ AbstractaContracts.missing_class_methods_for(self).freeze
59
+ end
60
+
61
+ def direct_interfaces
62
+ abstracta_contracts_direct_interfaces.dup.freeze
63
+ end
64
+
65
+ def interfaces
66
+ AbstractaContracts.interfaces_for(self).freeze
67
+ end
68
+
69
+ def implements?(interface)
70
+ AbstractaContracts.validate_interface!(interface)
71
+ interfaces.include?(interface)
72
+ end
73
+
74
+ def interface_methods
75
+ AbstractaContracts.required_interface_instance_methods_for(self).freeze
76
+ end
77
+
78
+ def interface_class_methods
79
+ AbstractaContracts.required_interface_class_methods_for(self).freeze
80
+ end
81
+
82
+ def missing_interface_methods
83
+ AbstractaContracts.missing_interface_instance_methods_for(self).freeze
84
+ end
85
+
86
+ def missing_interface_class_methods
87
+ AbstractaContracts.missing_interface_class_methods_for(self).freeze
88
+ end
89
+
90
+ def missing_methods
91
+ (missing_abstract_methods + missing_interface_methods).uniq.freeze
92
+ end
93
+
94
+ def missing_class_methods
95
+ (missing_abstract_class_methods + missing_interface_class_methods).uniq.freeze
96
+ end
97
+
98
+ def valid_implementation?
99
+ concrete?
100
+ end
101
+
102
+ def validate_implementation!
103
+ return true if concrete?
104
+
105
+ missing = { instance_methods: missing_methods, class_methods: missing_class_methods }
106
+ raise Internal::UnimplementedMethodsError.new(self, **missing) unless missing.values.all?(&:empty?)
107
+
108
+ raise Internal::AbstractClassInstantiationError, self
109
+ end
110
+
111
+ private
112
+
113
+ def abstracta_contracts_declared_instance_methods
114
+ @abstracta_contracts_declared_instance_methods ||= []
115
+ end
116
+
117
+ def abstracta_contracts_declared_class_methods
118
+ @abstracta_contracts_declared_class_methods ||= []
119
+ end
120
+
121
+ def abstracta_contracts_direct_interfaces
122
+ @abstracta_contracts_direct_interfaces ||= []
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractaContracts
4
+ module Internal
5
+ module ConstructorGuard
6
+ def new(...)
7
+ validate_implementation! if respond_to?(:validate_implementation!)
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractaContracts
4
+ module Internal
5
+ class Contract < Module
6
+ attr_reader :instance_methods, :class_methods
7
+
8
+ def initialize(instance_methods:, class_methods:)
9
+ super()
10
+ @instance_methods = instance_methods.freeze
11
+ @class_methods = class_methods.freeze
12
+ end
13
+
14
+ def included(base)
15
+ AbstractaContracts.install(base)
16
+ base.abstract_method(*instance_methods) unless instance_methods.empty?
17
+ base.abstract_class_method(*class_methods) unless class_methods.empty?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractaContracts
4
+ module Internal
5
+ module InterfaceDefinition
6
+ def interface?
7
+ true
8
+ end
9
+
10
+ def interface_methods
11
+ AbstractaContracts.interface_instance_methods_for(self)
12
+ end
13
+
14
+ def interface_class_methods
15
+ AbstractaContracts.interface_class_methods_for(self)
16
+ end
17
+ end
18
+
19
+ class Interface < Module
20
+ attr_reader :instance_methods, :class_methods
21
+
22
+ def initialize(instance_methods:, class_methods:)
23
+ super()
24
+ @instance_methods = instance_methods.freeze
25
+ @class_methods = class_methods.freeze
26
+ end
27
+
28
+ def included(base)
29
+ unless base.is_a?(Module) && !base.is_a?(Class)
30
+ raise TypeError, "AbstractaContracts.interface must be included in a module"
31
+ end
32
+
33
+ AbstractaContracts.define_interface(
34
+ base,
35
+ instance_methods: instance_methods,
36
+ class_methods: class_methods
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbstractaContracts
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "abstracta_contracts/version"
4
+ require_relative "abstracta_contracts/errors"
5
+ require_relative "abstracta_contracts/internal/class_methods"
6
+ require_relative "abstracta_contracts/internal/constructor_guard"
7
+ require_relative "abstracta_contracts/internal/contract"
8
+ require_relative "abstracta_contracts/internal/interface"
9
+
10
+ module AbstractaContracts
11
+ MUTEX_CREATION_LOCK = Mutex.new
12
+ private_constant :MUTEX_CREATION_LOCK
13
+
14
+ class << self
15
+ def included(base)
16
+ install(base)
17
+ end
18
+
19
+ def with_methods(*methods, class_methods: [])
20
+ instance_methods = normalize_method_names(methods)
21
+ singleton_methods = normalize_method_names(Array(class_methods))
22
+
23
+ if instance_methods.empty? && singleton_methods.empty?
24
+ raise ArgumentError, "at least one abstract instance or class method is required"
25
+ end
26
+
27
+ Internal::Contract.new(instance_methods: instance_methods, class_methods: singleton_methods)
28
+ end
29
+
30
+ def interface(*methods, class_methods: [])
31
+ instance_methods = normalize_method_names(methods)
32
+ singleton_methods = normalize_method_names(Array(class_methods))
33
+
34
+ if instance_methods.empty? && singleton_methods.empty?
35
+ raise ArgumentError, "at least one interface instance or class method is required"
36
+ end
37
+
38
+ Internal::Interface.new(instance_methods: instance_methods, class_methods: singleton_methods)
39
+ end
40
+
41
+ def install(base)
42
+ raise TypeError, "AbstractaContracts can only be included in classes" unless base.is_a?(Class)
43
+
44
+ base.extend(Internal::ClassMethods) unless base.singleton_class < Internal::ClassMethods
45
+ base.singleton_class.prepend(Internal::ConstructorGuard) unless base.singleton_class < Internal::ConstructorGuard
46
+ mutex_for(base)
47
+ base
48
+ end
49
+
50
+ def define_interface(base, instance_methods:, class_methods:)
51
+ if base.instance_variable_defined?(:@abstracta_contracts_interface_defined)
52
+ raise Internal::InterfaceAlreadyDefinedError,
53
+ "#{interface_name(base)} is already an AbstractaContracts interface"
54
+ end
55
+
56
+ base.instance_variable_set(:@abstracta_contracts_interface_defined, true)
57
+ base.instance_variable_set(:@abstracta_contracts_interface_instance_methods, instance_methods.freeze)
58
+ base.instance_variable_set(:@abstracta_contracts_interface_class_methods, class_methods.freeze)
59
+ base.extend(Internal::InterfaceDefinition) unless base.singleton_class < Internal::InterfaceDefinition
60
+ base
61
+ end
62
+
63
+ def interface?(object)
64
+ object.is_a?(Module) && object.respond_to?(:interface?) && object.interface?
65
+ end
66
+
67
+ def validate_interface!(interface)
68
+ return interface if interface?(interface)
69
+
70
+ raise Internal::InvalidInterfaceError, "#{interface.inspect} is not an AbstractaContracts interface"
71
+ end
72
+
73
+ def normalize_interfaces(interfaces)
74
+ interfaces.flatten.map { |interface| validate_interface!(interface) }.uniq.freeze
75
+ end
76
+
77
+ def normalize_method_names(names)
78
+ names.flatten.map do |name|
79
+ unless name.is_a?(String) || name.is_a?(Symbol)
80
+ raise Internal::InvalidMethodNameError, "abstract method names must be Strings or Symbols, got #{name.class}"
81
+ end
82
+
83
+ normalized = name.to_s
84
+ if normalized.empty? || normalized.match?(/\s/)
85
+ raise Internal::InvalidMethodNameError, "invalid abstract method name: #{name.inspect}"
86
+ end
87
+
88
+ normalized.to_sym
89
+ end.uniq.freeze
90
+ end
91
+
92
+ def register_instance_methods(base, names)
93
+ install(base)
94
+ synchronize(base) do
95
+ declared = base.send(:abstracta_contracts_declared_instance_methods)
96
+ names.each { |name| declared << name unless declared.include?(name) }
97
+ end
98
+ end
99
+
100
+ def register_class_methods(base, names)
101
+ install(base)
102
+ synchronize(base) do
103
+ declared = base.send(:abstracta_contracts_declared_class_methods)
104
+ names.each { |name| declared << name unless declared.include?(name) }
105
+ end
106
+ end
107
+
108
+ def required_instance_methods_for(klass)
109
+ required_methods_for(klass, :abstracta_contracts_declared_instance_methods)
110
+ end
111
+
112
+ def required_class_methods_for(klass)
113
+ required_methods_for(klass, :abstracta_contracts_declared_class_methods)
114
+ end
115
+
116
+ def missing_instance_methods_for(klass)
117
+ required_instance_methods_for(klass).filter_map do |name, declaration_owner|
118
+ name unless implemented_after_declaration?(klass, name, declaration_owner, singleton: false)
119
+ end
120
+ end
121
+
122
+ def missing_class_methods_for(klass)
123
+ required_class_methods_for(klass).filter_map do |name, declaration_owner|
124
+ name unless implemented_after_declaration?(klass, name, declaration_owner, singleton: true)
125
+ end
126
+ end
127
+
128
+ def interfaces_for(klass)
129
+ result = []
130
+
131
+ klass.ancestors.reverse_each do |ancestor|
132
+ next unless ancestor.is_a?(Class)
133
+ next unless ancestor.respond_to?(:abstracta_contracts_direct_interfaces, true)
134
+
135
+ ancestor.send(:abstracta_contracts_direct_interfaces).each do |interface|
136
+ interface_hierarchy(interface).each { |candidate| result << candidate unless result.include?(candidate) }
137
+ end
138
+ end
139
+
140
+ result
141
+ end
142
+
143
+ def interface_instance_methods_for(interface)
144
+ validate_interface!(interface)
145
+ interface_hierarchy(interface).flat_map do |candidate|
146
+ candidate.instance_variable_get(:@abstracta_contracts_interface_instance_methods) || []
147
+ end.uniq.freeze
148
+ end
149
+
150
+ def interface_class_methods_for(interface)
151
+ validate_interface!(interface)
152
+ interface_hierarchy(interface).flat_map do |candidate|
153
+ candidate.instance_variable_get(:@abstracta_contracts_interface_class_methods) || []
154
+ end.uniq.freeze
155
+ end
156
+
157
+ def required_interface_instance_methods_for(klass)
158
+ interfaces_for(klass).flat_map { |interface| interface_instance_methods_for(interface) }.uniq
159
+ end
160
+
161
+ def required_interface_class_methods_for(klass)
162
+ interfaces_for(klass).flat_map { |interface| interface_class_methods_for(interface) }.uniq
163
+ end
164
+
165
+ def missing_interface_instance_methods_for(klass)
166
+ required_interface_instance_methods_for(klass).reject do |name|
167
+ method_available?(klass, name, singleton: false)
168
+ end
169
+ end
170
+
171
+ def missing_interface_class_methods_for(klass)
172
+ required_interface_class_methods_for(klass).reject do |name|
173
+ method_available?(klass, name, singleton: true)
174
+ end
175
+ end
176
+
177
+ def synchronize(base, &)
178
+ mutex_for(base).synchronize(&)
179
+ end
180
+
181
+ def class_name(klass)
182
+ klass.name || klass.inspect
183
+ end
184
+
185
+ def interface_name(interface)
186
+ interface.name || interface.inspect
187
+ end
188
+
189
+ private
190
+
191
+ def mutex_for(base)
192
+ if base.instance_variable_defined?(:@abstracta_contracts_mutex)
193
+ return base.instance_variable_get(:@abstracta_contracts_mutex)
194
+ end
195
+
196
+ MUTEX_CREATION_LOCK.synchronize do
197
+ base.instance_variable_get(:@abstracta_contracts_mutex) ||
198
+ base.instance_variable_set(:@abstracta_contracts_mutex, Mutex.new)
199
+ end
200
+ end
201
+
202
+ def required_methods_for(klass, reader)
203
+ declarations = {}
204
+
205
+ klass.ancestors.reverse_each do |ancestor|
206
+ next unless ancestor.is_a?(Class)
207
+ next unless ancestor.respond_to?(reader, true)
208
+
209
+ ancestor.send(reader).each { |name| declarations[name] = ancestor }
210
+ end
211
+
212
+ declarations.freeze
213
+ end
214
+
215
+ def implemented_after_declaration?(klass, name, declaration_owner, singleton:)
216
+ lookup_class = singleton ? klass.singleton_class : klass
217
+ declaration_lookup_owner = singleton ? declaration_owner.singleton_class : declaration_owner
218
+
219
+ implementation_owner = lookup_class.instance_method(name).owner
220
+ return true if implementation_owner == declaration_lookup_owner
221
+
222
+ ancestors = lookup_class.ancestors
223
+ implementation_index = ancestors.index(implementation_owner)
224
+ declaration_index = ancestors.index(declaration_lookup_owner)
225
+
226
+ implementation_index && declaration_index && implementation_index < declaration_index
227
+ rescue NameError
228
+ false
229
+ end
230
+
231
+ def method_available?(klass, name, singleton:)
232
+ lookup = singleton ? klass.singleton_class : klass
233
+ lookup.instance_method(name)
234
+ true
235
+ rescue NameError
236
+ false
237
+ end
238
+
239
+ def interface_hierarchy(interface)
240
+ validate_interface!(interface)
241
+
242
+ interface.ancestors.reverse_each.with_object([]) do |ancestor, result|
243
+ next unless interface?(ancestor)
244
+
245
+ result << ancestor unless result.include?(ancestor)
246
+ end
247
+ end
248
+ end
249
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: abstracta-contracts
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Juan Furattini
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ AbstractaContracts provides lightweight, dependency-free abstract class contracts for Ruby,
14
+ including declarative instance/class methods, reusable interfaces, inherited contracts,
15
+ runtime validation, and introspection.
16
+ email: []
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ - lib/abstracta_contracts.rb
25
+ - lib/abstracta_contracts/errors.rb
26
+ - lib/abstracta_contracts/internal/class_methods.rb
27
+ - lib/abstracta_contracts/internal/constructor_guard.rb
28
+ - lib/abstracta_contracts/internal/contract.rb
29
+ - lib/abstracta_contracts/internal/interface.rb
30
+ - lib/abstracta_contracts/version.rb
31
+ homepage: https://github.com/Rubcraft/abstracta-contracts
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ homepage_uri: https://github.com/Rubcraft/abstracta-contracts
36
+ source_code_uri: https://github.com/Rubcraft/abstracta-contracts
37
+ changelog_uri: https://github.com/Rubcraft/abstracta-contracts/blob/main/CHANGELOG.md
38
+ allowed_push_host: https://rubygems.org
39
+ rubygems_mfa_required: 'true'
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.2'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 4.0.16
55
+ specification_version: 4
56
+ summary: Declarative abstract classes, interfaces, and method contracts for Ruby.
57
+ test_files: []