singulus 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: ac98626fb3bc3db2c925dc5a31d25655501aa36544907ee9ba95d87b3e7e3917
4
+ data.tar.gz: 371565dd472005e3a4038a0fd7ad1d9fa2679fe237b610072101c5a541b7f821
5
+ SHA512:
6
+ metadata.gz: 5a6826300e20a88e8ae1e811cd1525c7f3cd38dcb6628d22608c92e2fc9ebc7029774288b2b990742783222f98f9054d188e5d892852a3bc7da8cf74181f71cc
7
+ data.tar.gz: 8d7b79fbcaac93d423b22c82856539e57c6ed3f718cbed8a22a1ca2f6760c45d2e47c42c1db3a11bfb1bda1033cf6800cfcf3029474693add6f54e8fb876855f
data/CHANGELOG.md ADDED
@@ -0,0 +1,77 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Fixed
10
+
11
+ - Completed the final RuboCop cleanup across Multiton and specs.
12
+ - Removed `refuse_coverage_drop`; coverage is enforced by absolute minimums of 95% line and 90% branch.
13
+
14
+ - Normalized RuboCop policy so local Rubcraft checks and GitHub CI enforce the same repository-local configuration.
15
+ - Fixed production-code indentation/alignment issues introduced during the `Singulus::Internal` refactor.
16
+ - Removed the redundant `thread` require.
17
+ - Added RubyGems MFA metadata.
18
+ - Moved development tooling declarations to Gemfile.
19
+ - Kept intentional reflection interception, internal ivar naming, Multiton complexity, and contract-style specs explicitly documented in RuboCop configuration.
20
+
21
+ - Added a small coverage buffer for safe mutation/reflection fallthrough paths and repeated runtime-hardening activation.
22
+
23
+ - Made RuboCop configuration explicit and repository-local for identical local/CI behavior.
24
+ - CI and release workflows now pass `.rubocop.yml` explicitly.
25
+ - Standardized the project on double-quoted Ruby strings.
26
+
27
+ - Added `bundler-audit` as a development dependency so Rubcraft's mandatory audit gate runs through the repository bundle.
28
+
29
+ - Fixed `InstanceGuard#clone` so it does not forward an implicit `freeze:` keyword to Ruby Singleton implementations that accept no arguments.
30
+ - Corrected standard-mode reflection coverage: reflective constructor access is intentionally available when local hardening is disabled.
31
+ - Corrected runtime-hardening coverage to distinguish blocked reflection gateways from safe non-gateway UnboundMethod binding.
32
+
33
+ - Added branch-focused behavior coverage for standard-mode fallthroughs, reflection guards, runtime-hardening negative paths, and valid Multiton retention branches.
34
+
35
+ - Corrected runtime Method composition specs to distinguish `Method#<<`/`Method#>>` from `Proc#<<` composition semantics.
36
+
37
+ - Expanded behavioral coverage for configuration normalization, public reflection guards, Multiton registry edge cases, forwarding semantics, standard-mode inheritance, and runtime Method composition.
38
+
39
+ - Corrected RSpec expectations for sealed constructor access.
40
+ - Corrected Multiton key-stabilization coverage to use genuinely mutable input under `frozen_string_literal`.
41
+ - Corrected Hash identifiers to be passed positionally under Ruby 3 keyword argument semantics.
42
+
43
+ ## [0.1.0] - 2026-08-21
44
+
45
+ ### Changed
46
+
47
+ - Reduced the public root API to `Singulus::Singleton`, `Singulus::Multiton`, `Singulus::Error`, `Singulus::VERSION`, and the configuration facade.
48
+ - Moved implementation classes, guards, configuration objects, and specialized exceptions behind a private `Singulus::Internal` namespace.
49
+ - Marked Multiton implementation constants (`RETENTIONS`, `Entry`, and `ClassMethods`) private.
50
+ - Added a public API contract spec to prevent accidental constant leakage.
51
+
52
+ ### Added
53
+
54
+ - `Singulus::Singleton` for one managed instance per class.
55
+ - `Singulus::Multiton` for one managed instance per normalized identifier.
56
+ - `:standard`, `:strict`, and `:runtime` hardening modes.
57
+ - Include-time configuration with `Singulus::Singleton.with(...)` and `Singulus::Multiton.with(...)`.
58
+ - Two-step `singulus` DSL in addition to include-time `.with(...)` configuration.
59
+ - Multiton key normalization and defensive stabilization of common mutable keys.
60
+ - `:forever`, `:lru`, `:ttl`, `:bounded`, and `:weak` Multiton retention strategies.
61
+ - Explicit Multiton lifecycle operations: `instance?`, `instance_count`, `instance_keys`, `delete_instance`, and `clear_instances`.
62
+ - Thread-safe Singleton and Multiton initialization.
63
+ - Recursive Multiton initialization detection.
64
+ - Constructor, duplication, inheritance, mutation, and reflection guards in hardened modes.
65
+ - Runtime protection for common captured `Method` and `UnboundMethod` constructor/reflection paths.
66
+ - RSpec, RuboCop, SimpleCov, GitHub Actions CI, and RubyGems Trusted Publishing workflow.
67
+
68
+ ### Security
69
+
70
+ - Guard `new` and `allocate` from the moment hardened Singulus modules are included.
71
+ - Prevent constructor bypass by changing constructor visibility.
72
+ - Reject constructor redefinition attempts in hardened modes.
73
+ - Protect common reflective constructor access paths.
74
+ - Protect captured `Method` and `UnboundMethod` paths in `:runtime` mode.
75
+
76
+ [Unreleased]: https://github.com/Rubcraft/singulus/compare/v0.1.0...HEAD
77
+ [0.1.0]: https://github.com/Rubcraft/singulus/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,248 @@
1
+ # Singulus
2
+
3
+ [![CI](https://github.com/Rubcraft/singulus/actions/workflows/ci.yml/badge.svg)](https://github.com/Rubcraft/singulus/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/singulus.svg)](https://rubygems.org/gems/singulus)
5
+
6
+
7
+ `Singulus` adds configurable Ruby-level hardening around two instance-uniqueness patterns:
8
+
9
+ - `Singulus::Singleton` — one instance per class and Ruby process.
10
+ - `Singulus::Multiton` — one registered instance per class and normalized key.
11
+
12
+ It builds on Ruby's standard `Singleton` for the classic Singleton implementation and provides its own thread-safe keyed registry for Multiton.
13
+
14
+ Singulus exposes one supported entry point:
15
+
16
+ ```ruby
17
+ require "singulus"
18
+ ```
19
+
20
+ The supported root API is intentionally small:
21
+
22
+ - `Singulus::Singleton`
23
+ - `Singulus::Multiton`
24
+ - `Singulus::Error` as the public base exception
25
+ - `Singulus.configure`, `Singulus.configuration`, and `Singulus.reset_configuration!`
26
+ - `Singulus::VERSION`
27
+
28
+ All implementation classes, guards, configuration types, and specialized errors live behind a private `Singulus::Internal` namespace and are not public constants. No pre-release compatibility entry points are shipped.
29
+
30
+ ## Security boundary
31
+
32
+ This gem is aggressive Ruby-level hardening, **not a sandbox**. Code executing in the same Ruby process can reopen core classes, use native extensions, replace guards, or otherwise modify the runtime. `:runtime` closes many common `Method`/`UnboundMethod` bypasses, but it cannot create a cryptographic or process-isolation boundary.
33
+
34
+ ## Modes
35
+
36
+ ```ruby
37
+ Singulus.configure do |config|
38
+ config.default_mode = :strict
39
+ end
40
+ ```
41
+
42
+ Available modes:
43
+
44
+ - `:standard` — minimal policy. Classic Singleton delegates to Ruby `Singleton`; Multiton keeps constructors private but does not install local hardening policy.
45
+ - `:strict` — constructor reflection guards, mutation protection, duplication protection, inheritance restrictions, and constructor sealing for classic Singleton.
46
+ - `:runtime` — everything in `:strict`, plus process-wide `Method` and `UnboundMethod` guards that activate only for classes marked as runtime-hardened.
47
+
48
+ The default is `:strict`.
49
+
50
+ ### Configure directly from `include`
51
+
52
+ `Singleton` and `Multiton` also expose `.with(...)`, which creates an independent configured inclusion module. It does not mutate the shared `Singulus::Singleton` or `Singulus::Multiton` module.
53
+
54
+ ```ruby
55
+ class Configuration
56
+ include Singulus::Singleton.with(:strict)
57
+ end
58
+
59
+ class RuntimeConfiguration
60
+ include Singulus::Singleton.with(mode: :runtime)
61
+ end
62
+
63
+ class TenantConfiguration
64
+ include Singulus::Multiton.with(
65
+ :runtime,
66
+ retention: :lru,
67
+ max_size: 5_000
68
+ )
69
+ end
70
+ ```
71
+
72
+ The existing two-step DSL remains supported and equivalent:
73
+
74
+ ```ruby
75
+ include Singulus::Singleton
76
+ singulus mode: :strict
77
+ ```
78
+
79
+ ## Classic Singleton
80
+
81
+ ```ruby
82
+ require "singulus"
83
+
84
+ class Configuration
85
+ include Singulus::Singleton.with(:runtime)
86
+ end
87
+
88
+ Configuration.instance.equal?(Configuration.instance)
89
+ # => true
90
+ ```
91
+
92
+ In hardened modes, direct constructor access is guarded from the moment the module is included, including attempts to change constructor visibility. Common reflection routes, duplication, constructor redefinition, and inheritance are rejected. After the first `instance`, classic Singleton also seals its constructor lookup path.
93
+
94
+ ## Multiton: one instance per identifier
95
+
96
+ ```ruby
97
+ class TenantConfiguration
98
+ include Singulus::Multiton.with(:strict)
99
+
100
+ def initialize(tenant_id)
101
+ @tenant_id = tenant_id
102
+ end
103
+ end
104
+
105
+ one = TenantConfiguration.instance_for(10)
106
+ two = TenantConfiguration.instance_for(10)
107
+ other = TenantConfiguration.instance_for(20)
108
+
109
+ one.equal?(two) # => true
110
+ one.equal?(other) # => false
111
+ ```
112
+
113
+ `instance_for(identifier, *args, **kwargs)` passes the original identifier as the first initializer argument. Additional initializer arguments are used only when an instance for that normalized key does not already exist.
114
+
115
+ ### Stable tenant keys
116
+
117
+ Prefer immutable identifiers such as integers, symbols, UUID strings, or normalized composite keys. Strings, arrays, and hashes returned as keys are defensively duplicated/frozen recursively where practical.
118
+
119
+ For domain objects, define a key normalizer:
120
+
121
+ ```ruby
122
+ class TenantConfiguration
123
+ include Singulus::Multiton
124
+
125
+ multiton_key { |tenant| tenant.id }
126
+
127
+ def initialize(tenant)
128
+ @tenant = tenant
129
+ end
130
+ end
131
+ ```
132
+
133
+ This avoids retaining an ActiveRecord object itself as the registry key.
134
+
135
+ ## Multiton lifecycle
136
+
137
+ ```ruby
138
+ TenantConfiguration.instance?(10)
139
+ TenantConfiguration.instance_count
140
+ TenantConfiguration.instance_keys
141
+ TenantConfiguration.delete_instance(10)
142
+ TenantConfiguration.clear_instances
143
+ ```
144
+
145
+ `delete_instance` and `clear_instances` explicitly release registry ownership. If another part of the application still holds the old object, a later `instance_for` can create a new object for the same key. Therefore uniqueness is **registry/lifecycle scoped**, not a claim that no second live Ruby object can ever exist after explicit release or eviction.
146
+
147
+ ## Retention strategies
148
+
149
+ The default is strict registry retention:
150
+
151
+ ```ruby
152
+ class TenantConfiguration
153
+ include Singulus::Multiton
154
+ multiton_retention :forever
155
+ end
156
+ ```
157
+
158
+ Supported strategies:
159
+
160
+ ### `:forever`
161
+
162
+ Keeps each registered instance until `delete_instance`/`clear_instances` or process exit. This is the strongest per-key identity guarantee supplied by Multiton.
163
+
164
+ ### `:lru`
165
+
166
+ ```ruby
167
+ multiton_retention :lru, max_size: 5_000
168
+ ```
169
+
170
+ Bounds memory by evicting the least recently used registry entry. Once evicted, the same key can create a new instance later.
171
+
172
+ ### `:ttl`
173
+
174
+ ```ruby
175
+ multiton_retention :ttl, ttl: 300
176
+ ```
177
+
178
+ Keeps an instance for a fixed TTL measured with `Process::CLOCK_MONOTONIC`. Expiration is based on creation time, not sliding access time. After expiration, the same key can create a new instance.
179
+
180
+ ### `:bounded`
181
+
182
+ ```ruby
183
+ multiton_retention :bounded, ttl: 300, max_size: 5_000
184
+ ```
185
+
186
+ Combines TTL expiration with an LRU capacity bound. Entries are evicted when they expire or when the registry exceeds `max_size`. Use this instead of passing `max_size` to `:ttl` or `ttl` to `:lru`; those ambiguous configurations are rejected.
187
+
188
+ ### `:weak`
189
+
190
+ ```ruby
191
+ multiton_retention :weak
192
+ ```
193
+
194
+ Stores weak references. Singulus reuses the same object while it remains alive elsewhere, but the garbage collector may reclaim it when no strong references remain. A later `instance_for` can therefore create a new instance. `:weak` accepts neither `ttl` nor `max_size`.
195
+
196
+ | Strategy | `ttl` | `max_size` | Identity scope |
197
+ | --- | --- | --- | --- |
198
+ | `:forever` | no | no | until explicit release/process exit |
199
+ | `:lru` | no | required | while retained by capacity |
200
+ | `:ttl` | required | no | until expiration |
201
+ | `:bounded` | required | required | until expiration or capacity eviction |
202
+ | `:weak` | no | no | while the object remains strongly referenced |
203
+
204
+ `multiton_key` and retention configuration are locked while the registry contains instances. Call `clear_instances` before changing them deliberately.
205
+
206
+ ## Runtime hardening
207
+
208
+ Runtime mode additionally guards common bypasses involving:
209
+
210
+ - `Method#call`
211
+ - `Method#[]`
212
+ - `Method#to_proc`
213
+ - `Method#>>` / `Method#<<`
214
+ - `UnboundMethod#bind`
215
+ - `UnboundMethod#bind_call`
216
+ - captured reflection gateways such as `Object#method` and `BasicObject#__send__`
217
+
218
+ The patches are installed process-wide once required, but their rejection logic only activates for classes marked `:runtime`.
219
+
220
+ Enable runtime mode as early as possible during boot:
221
+
222
+ ```ruby
223
+ Singulus.configure do |config|
224
+ config.default_mode = :runtime
225
+ end
226
+ ```
227
+
228
+ For Rails, place this in an early initializer before code that might capture constructor references.
229
+
230
+ A `Proc` created from a constructor `Method` *before* Singulus runtime hardening is enabled cannot be retroactively identified as constructor-derived. Enable `:runtime` during boot, before untrusted or extension code can capture such references.
231
+
232
+ ## Development
233
+
234
+ ```bash
235
+ bundle install
236
+ bundle exec rubocop --parallel
237
+ bundle exec rspec
238
+ bundle exec bundler-audit check --update
239
+ bundle exec rake build
240
+ ```
241
+
242
+ SimpleCov enforces line and branch coverage, while CI tests supported Ruby versions independently from RuboCop.
243
+
244
+ ## Releasing
245
+
246
+ Repository initialization, version-control operations, and release-tag creation are managed by the Rubcraft Toolkit. Singulus itself does not prescribe or duplicate those commands.
247
+
248
+ The repository keeps only the CI/publish boundary: `.github/workflows/release.yml` reacts to a version tag produced by the Toolkit, verifies that it matches `Singulus::VERSION`, runs quality checks, builds the gem, and publishes through RubyGems Trusted Publishing (OIDC). Configure a GitHub environment named `release` and the matching RubyGems Trusted Publisher before the first release.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Singulus
4
+ module Internal
5
+ class Configuration
6
+ MODES = %i[standard strict runtime].freeze
7
+
8
+ attr_reader :default_mode
9
+
10
+ def initialize
11
+ self.default_mode = :strict
12
+ end
13
+
14
+ def default_mode=(mode)
15
+ mode = normalize_mode(mode)
16
+
17
+ unless MODES.include?(mode)
18
+ raise InvalidModeError,
19
+ "invalid Singulus mode #{mode.inspect}; expected one of: #{MODES.join(', ')}"
20
+ end
21
+
22
+ @default_mode = mode
23
+ end
24
+
25
+ private
26
+
27
+ def normalize_mode(mode)
28
+ mode.to_sym
29
+ rescue NoMethodError
30
+ mode
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Singulus
4
+ module Internal
5
+ module ConstructorGuard
6
+ private
7
+
8
+ def new(...)
9
+ singulus_reject_unauthorized_constructor!(:new)
10
+ super
11
+ end
12
+
13
+ def allocate
14
+ singulus_reject_unauthorized_constructor!(:allocate)
15
+ super
16
+ end
17
+
18
+ def singulus_reject_unauthorized_constructor!(method_name)
19
+ return unless Internal.locally_hardened?(self)
20
+ return if Internal.constructor_access_allowed?(self)
21
+
22
+ raise ConstructorAccessError,
23
+ "constructor #{method_name.inspect} is inaccessible for #{Internal.kind_for(self)} #{self}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Singulus
4
+ class Error < StandardError; end
5
+
6
+ module Internal
7
+ class ConstructorAccessError < Singulus::Error; end
8
+ class DuplicationError < Singulus::Error; end
9
+ class InheritanceError < Singulus::Error; end
10
+ class InvalidModeError < Singulus::Error; end
11
+ class InvalidRetentionError < Singulus::Error; end
12
+ class ConfigurationLockedError < Singulus::Error; end
13
+ class RecursiveInitializationError < Singulus::Error; end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Singulus
4
+ module Internal
5
+ module InstanceGuard
6
+ def dup
7
+ singulus_reject_duplication!(:duplicated)
8
+ super
9
+ end
10
+
11
+ def clone(**options)
12
+ singulus_reject_duplication!(:cloned)
13
+ return super() if options.empty?
14
+
15
+ super
16
+ end
17
+
18
+ def initialize_dup(other)
19
+ singulus_reject_duplication!(:duplicated)
20
+ super
21
+ end
22
+
23
+ def initialize_clone(other, **kwargs)
24
+ singulus_reject_duplication!(:cloned)
25
+ super
26
+ end
27
+
28
+ private
29
+
30
+ def singulus_reject_duplication!(operation)
31
+ return unless Internal.locally_hardened?(self.class)
32
+
33
+ raise DuplicationError, "#{self.class} instance cannot be #{operation} in hardened mode"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Singulus
4
+ module Internal
5
+ module MutationGuard
6
+ def singleton_method_added(method_name)
7
+ return super if @__singulus_restoring_constructor__
8
+ return super unless Internal.locally_hardened?(self)
9
+ return super unless Internal.constructor_name?(method_name)
10
+
11
+ singulus_restore_constructor!(method_name)
12
+
13
+ raise ConstructorAccessError,
14
+ "constructor #{method_name.inspect} cannot be redefined on #{Internal.kind_for(self)} #{self}"
15
+ end
16
+
17
+ private
18
+
19
+ def singulus_restore_constructor!(method_name)
20
+ @__singulus_restoring_constructor__ = true
21
+ singleton_class.__send__(:remove_method, method_name)
22
+
23
+ if Internal.sealed_singleton?(self)
24
+ singleton_class.__send__(:undef_method, method_name)
25
+ else
26
+ private_class_method method_name
27
+ end
28
+ ensure
29
+ @__singulus_restoring_constructor__ = false
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Singulus
4
+ module Internal
5
+ module ReflectionGuard
6
+ def method(method_name)
7
+ singulus_reject_constructor_access!(method_name)
8
+ super
9
+ end
10
+
11
+ def public_method(method_name)
12
+ singulus_reject_constructor_access!(method_name)
13
+ super
14
+ end
15
+
16
+ def singleton_method(method_name)
17
+ singulus_reject_constructor_access!(method_name)
18
+ super
19
+ end
20
+
21
+ def send(method_name, ...)
22
+ singulus_reject_constructor_access!(method_name)
23
+ super
24
+ end
25
+
26
+ def __send__(method_name, ...)
27
+ singulus_reject_constructor_access!(method_name)
28
+ super
29
+ end
30
+
31
+ def public_send(method_name, ...)
32
+ singulus_reject_constructor_access!(method_name)
33
+ super
34
+ end
35
+
36
+ private
37
+
38
+ def singulus_reject_constructor_access!(method_name)
39
+ return unless Internal.locally_hardened?(self)
40
+ return unless Internal.constructor_name?(method_name)
41
+
42
+ raise ConstructorAccessError,
43
+ "constructor #{method_name.inspect} is inaccessible for #{Internal.kind_for(self)} #{self}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Singulus
4
+ module Internal
5
+ module RuntimeHardening
6
+ REFLECTION_GATEWAYS = %i[
7
+ method
8
+ public_method
9
+ singleton_method
10
+ send
11
+ __send__
12
+ public_send
13
+ ].freeze
14
+
15
+ module UnboundMethodGuard
16
+ def bind(object)
17
+ singulus_reject_binding!(object)
18
+ super
19
+ end
20
+
21
+ def bind_call(object, *args, &)
22
+ singulus_reject_bind_call!(object, args)
23
+ super
24
+ end
25
+
26
+ private
27
+
28
+ def singulus_reject_binding!(object)
29
+ return unless Internal.runtime_hardened?(object)
30
+ return unless Internal.constructor_name?(name) || RuntimeHardening.reflection_gateway?(name)
31
+
32
+ raise ConstructorAccessError,
33
+ "cannot bind #{owner}##{name} to runtime-hardened Singulus #{object}"
34
+ end
35
+
36
+ def singulus_reject_bind_call!(object, args)
37
+ return unless Internal.runtime_hardened?(object)
38
+
39
+ if Internal.constructor_name?(name)
40
+ raise ConstructorAccessError,
41
+ "cannot invoke constructor #{owner}##{name} on runtime-hardened Singulus #{object}"
42
+ end
43
+
44
+ return unless RuntimeHardening.reflection_gateway?(name)
45
+ return unless Internal.constructor_name?(args.first)
46
+
47
+ raise ConstructorAccessError,
48
+ "cannot use #{owner}##{name} to access constructor #{args.first.inspect} " \
49
+ "on runtime-hardened Singulus #{object}"
50
+ end
51
+ end
52
+
53
+ module MethodGuard
54
+ def call(*args, &)
55
+ singulus_reject_invocation!(args)
56
+ super
57
+ end
58
+
59
+ def [](*args, &)
60
+ singulus_reject_invocation!(args)
61
+ super
62
+ end
63
+
64
+ def to_proc
65
+ singulus_reject_transformation!
66
+ super
67
+ end
68
+
69
+ def >>(other)
70
+ singulus_reject_transformation!
71
+ super
72
+ end
73
+
74
+ def <<(other)
75
+ singulus_reject_transformation!
76
+ super
77
+ end
78
+
79
+ private
80
+
81
+ def singulus_reject_invocation!(args)
82
+ return unless Internal.runtime_hardened?(receiver)
83
+
84
+ if Internal.constructor_name?(name)
85
+ raise ConstructorAccessError,
86
+ "constructor #{name.inspect} cannot be invoked for runtime-hardened Singulus #{receiver}"
87
+ end
88
+
89
+ return unless RuntimeHardening.reflection_gateway?(name)
90
+ return unless Internal.constructor_name?(args.first)
91
+
92
+ raise ConstructorAccessError,
93
+ "#{name} cannot be used to access constructor #{args.first.inspect} " \
94
+ "for runtime-hardened Singulus #{receiver}"
95
+ end
96
+
97
+ def singulus_reject_transformation!
98
+ return unless Internal.runtime_hardened?(receiver)
99
+ return unless Internal.constructor_name?(name) || RuntimeHardening.reflection_gateway?(name)
100
+
101
+ raise ConstructorAccessError,
102
+ "Method #{name.inspect} for runtime-hardened Singulus #{receiver} " \
103
+ "cannot be converted or composed"
104
+ end
105
+ end
106
+
107
+ class << self
108
+ def enable!
109
+ mutex.synchronize do
110
+ return if enabled?
111
+
112
+ UnboundMethod.prepend(UnboundMethodGuard)
113
+ Method.prepend(MethodGuard)
114
+
115
+ @enabled = true
116
+ end
117
+ end
118
+
119
+ def enabled?
120
+ @enabled == true
121
+ end
122
+
123
+ def reflection_gateway?(method_name)
124
+ REFLECTION_GATEWAYS.include?(Internal.normalize_method_name(method_name))
125
+ end
126
+
127
+ private
128
+
129
+ def mutex
130
+ @mutex ||= Thread::Mutex.new
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Singulus
4
+ module Internal
5
+ module SingletonClassMethods
6
+ def instance
7
+ return super unless Internal.locally_hardened?(self)
8
+
9
+ instance = Internal.with_constructor_access(self) { super }
10
+ singulus_seal_constructors!
11
+ instance
12
+ end
13
+
14
+ def singulus(mode:)
15
+ Internal.set_mode!(self, mode)
16
+ self
17
+ end
18
+
19
+ def singulus_mode
20
+ Internal.mode_for(self)
21
+ end
22
+
23
+ private
24
+
25
+ def inherited(subclass)
26
+ if Internal.locally_hardened?(self)
27
+ raise InheritanceError,
28
+ "#{self} is a Singulus in #{singulus_mode.inspect} mode and cannot be subclassed"
29
+ end
30
+
31
+ super
32
+ end
33
+
34
+ def singulus_seal_constructors!
35
+ return if singulus_sealed?
36
+
37
+ singulus_seal_mutex.synchronize do
38
+ return if singulus_sealed?
39
+
40
+ singulus_seal_constructor!(:new)
41
+ singulus_seal_constructor!(:allocate)
42
+
43
+ @__singulus_sealed__ = true
44
+ end
45
+ end
46
+
47
+ def singulus_seal_constructor!(method_name)
48
+ singleton_class.__send__(:undef_method, method_name)
49
+ rescue NameError
50
+ return unless respond_to?(method_name, true)
51
+
52
+ raise
53
+ end
54
+
55
+ def singulus_sealed?
56
+ @__singulus_sealed__ == true
57
+ end
58
+
59
+ def singulus_seal_mutex
60
+ @__singulus_seal_mutex__ ||= Thread::Mutex.new
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Singulus
4
+ module Multiton
5
+ RETENTIONS = %i[forever lru ttl bounded weak].freeze
6
+ private_constant :RETENTIONS
7
+
8
+ Entry = Struct.new(:value, :expires_at, keyword_init: true) do
9
+ def instance
10
+ value.is_a?(WeakRef) ? value.__getobj__ : value
11
+ rescue WeakRef::RefError
12
+ nil
13
+ end
14
+
15
+ def alive?
16
+ !instance.nil?
17
+ end
18
+ end
19
+ private_constant :Entry
20
+
21
+ class << self
22
+ def included(base)
23
+ install(base)
24
+ end
25
+
26
+ def with(mode = nil, **options)
27
+ resolved_mode = options.delete(:mode)
28
+
29
+ raise ArgumentError, "mode must be provided either positionally or as mode:, not both" if mode && resolved_mode
30
+
31
+ resolved_mode ||= mode || Singulus.configuration.default_mode
32
+ retention = options.delete(:retention)
33
+ ttl = options.delete(:ttl)
34
+ max_size = options.delete(:max_size)
35
+
36
+ raise ArgumentError, "unknown Multiton options: #{options.keys.map(&:inspect).join(', ')}" unless options.empty?
37
+
38
+ Module.new do
39
+ define_singleton_method(:included) do |base|
40
+ Multiton.install(base, mode: resolved_mode)
41
+ base.singulus(retention: retention, ttl: ttl, max_size: max_size) if retention
42
+ end
43
+ end
44
+ end
45
+
46
+ def install(base, mode: Singulus.configuration.default_mode)
47
+ Internal.initialize_managed_class!(base, kind: :multiton, mode: mode)
48
+
49
+ base.include Internal::InstanceGuard
50
+ base.private_class_method :new, :allocate
51
+ base.singleton_class.prepend Internal::ReflectionGuard
52
+ base.singleton_class.prepend Internal::MutationGuard
53
+ base.singleton_class.prepend Internal::ConstructorGuard
54
+ base.extend ClassMethods
55
+
56
+ initialize_registry_state(base)
57
+ Internal::RuntimeHardening.enable! if Internal.runtime_hardened?(base)
58
+ base
59
+ end
60
+
61
+ private
62
+
63
+ def initialize_registry_state(base)
64
+ base.instance_variable_set(:@__singulus_multiton_mutex__, Monitor.new)
65
+ base.instance_variable_set(:@__singulus_multiton_instances__, {})
66
+ base.instance_variable_set(:@__singulus_multiton_key_normalizer__, nil)
67
+ base.instance_variable_set(:@__singulus_multiton_retention__, :forever)
68
+ base.instance_variable_set(:@__singulus_multiton_ttl__, nil)
69
+ base.instance_variable_set(:@__singulus_multiton_max_size__, nil)
70
+ base.instance_variable_set(:@__singulus_multiton_initializing_keys__, {})
71
+ end
72
+ end
73
+
74
+ module ClassMethods
75
+ def instance_for(identifier, ...)
76
+ key = singulus_multiton_key_for(identifier)
77
+
78
+ singulus_multiton_mutex.synchronize do
79
+ singulus_multiton_purge_expired!
80
+
81
+ if (entry = singulus_multiton_instances[key])
82
+ if (instance = entry.instance)
83
+ singulus_multiton_touch_lru!(key, entry)
84
+ return instance
85
+ end
86
+
87
+ singulus_multiton_instances.delete(key)
88
+ end
89
+
90
+ singulus_multiton_reject_recursive_initialization!(key)
91
+ singulus_multiton_initializing_keys[key] = true
92
+
93
+ begin
94
+ instance = Internal.with_constructor_access(self) do
95
+ new(identifier, ...)
96
+ end
97
+ singulus_multiton_store!(key, instance)
98
+ instance
99
+ ensure
100
+ singulus_multiton_initializing_keys.delete(key)
101
+ end
102
+ end
103
+ end
104
+
105
+ def instance?(identifier)
106
+ key = singulus_multiton_key_for(identifier)
107
+
108
+ singulus_multiton_mutex.synchronize do
109
+ singulus_multiton_purge_expired!
110
+ singulus_multiton_instances.key?(key)
111
+ end
112
+ end
113
+
114
+ def delete_instance(identifier)
115
+ key = singulus_multiton_key_for(identifier)
116
+
117
+ singulus_multiton_mutex.synchronize do
118
+ singulus_multiton_purge_expired!
119
+ singulus_multiton_instances.delete(key)&.instance
120
+ end
121
+ end
122
+
123
+ def clear_instances
124
+ singulus_multiton_mutex.synchronize do
125
+ singulus_multiton_purge_expired!
126
+ count = singulus_multiton_instances.length
127
+ singulus_multiton_instances.clear
128
+ count
129
+ end
130
+ end
131
+
132
+ def instance_count
133
+ singulus_multiton_mutex.synchronize do
134
+ singulus_multiton_purge_expired!
135
+ singulus_multiton_instances.length
136
+ end
137
+ end
138
+
139
+ def instance_keys
140
+ singulus_multiton_mutex.synchronize do
141
+ singulus_multiton_purge_expired!
142
+ singulus_multiton_instances.keys.dup.freeze
143
+ end
144
+ end
145
+
146
+ def singulus(mode: nil, retention: nil, ttl: nil, max_size: nil)
147
+ Internal.set_mode!(self, mode) if mode
148
+ multiton_retention(retention, ttl: ttl, max_size: max_size) if retention
149
+ self
150
+ end
151
+
152
+ def singulus_mode
153
+ Internal.mode_for(self)
154
+ end
155
+
156
+ def multiton_key(&block)
157
+ return @__singulus_multiton_key_normalizer__ unless block
158
+
159
+ singulus_multiton_assert_registry_empty!("key normalizer")
160
+ @__singulus_multiton_key_normalizer__ = block
161
+ self
162
+ end
163
+
164
+ def multiton_retention(strategy = nil, ttl: nil, max_size: nil)
165
+ return @__singulus_multiton_retention__ unless strategy
166
+
167
+ strategy = Internal.normalize_method_name(strategy)
168
+ unless RETENTIONS.include?(strategy)
169
+ raise Internal::InvalidRetentionError,
170
+ "invalid Multiton retention #{strategy.inspect}; expected one of: #{RETENTIONS.join(', ')}"
171
+ end
172
+
173
+ singulus_multiton_validate_retention_options!(strategy, ttl: ttl, max_size: max_size)
174
+ singulus_multiton_assert_registry_empty!("retention")
175
+
176
+ @__singulus_multiton_retention__ = strategy
177
+ @__singulus_multiton_ttl__ = ttl&.to_f
178
+ @__singulus_multiton_max_size__ = max_size&.to_i
179
+ self
180
+ end
181
+
182
+ private
183
+
184
+ def inherited(subclass)
185
+ if Internal.locally_hardened?(self)
186
+ raise Internal::InheritanceError,
187
+ "#{self} is a Multiton in #{singulus_mode.inspect} mode and cannot be subclassed"
188
+ end
189
+
190
+ super
191
+ Multiton.install(subclass, mode: singulus_mode)
192
+ end
193
+
194
+ def singulus_multiton_instances
195
+ @__singulus_multiton_instances__ ||= {}
196
+ end
197
+
198
+ def singulus_multiton_mutex
199
+ @__singulus_multiton_mutex__ ||= Monitor.new
200
+ end
201
+
202
+ def singulus_multiton_initializing_keys
203
+ @__singulus_multiton_initializing_keys__ ||= {}
204
+ end
205
+
206
+ def singulus_multiton_key_for(identifier)
207
+ normalizer = @__singulus_multiton_key_normalizer__
208
+ key = normalizer ? normalizer.call(identifier) : identifier
209
+ singulus_multiton_stabilize_key(key)
210
+ end
211
+
212
+ def singulus_multiton_stabilize_key(key)
213
+ case key
214
+ when String
215
+ key.dup.freeze
216
+ when Array
217
+ key.map { |item| singulus_multiton_stabilize_key(item) }.freeze
218
+ when Hash
219
+ key.each_with_object({}) do |(hash_key, value), stabilized|
220
+ stabilized[singulus_multiton_stabilize_key(hash_key)] = singulus_multiton_stabilize_key(value)
221
+ end.freeze
222
+ else
223
+ key
224
+ end
225
+ end
226
+
227
+ def singulus_multiton_store!(key, instance)
228
+ retention = @__singulus_multiton_retention__
229
+ ttl_retention = %i[ttl bounded].include?(retention)
230
+ expires_at = singulus_multiton_monotonic_time + @__singulus_multiton_ttl__ if ttl_retention
231
+ value = retention == :weak ? WeakRef.new(instance) : instance
232
+
233
+ singulus_multiton_instances[key] = Entry.new(value: value, expires_at: expires_at)
234
+ singulus_multiton_evict_lru! if %i[lru bounded].include?(retention)
235
+ end
236
+
237
+ def singulus_multiton_touch_lru!(key, entry)
238
+ return unless %i[lru bounded].include?(@__singulus_multiton_retention__)
239
+
240
+ singulus_multiton_instances.delete(key)
241
+ singulus_multiton_instances[key] = entry
242
+ end
243
+
244
+ def singulus_multiton_evict_lru!
245
+ max_size = @__singulus_multiton_max_size__
246
+ singulus_multiton_instances.shift while singulus_multiton_instances.length > max_size
247
+ end
248
+
249
+ def singulus_multiton_purge_expired!
250
+ retention = @__singulus_multiton_retention__
251
+
252
+ if %i[ttl bounded].include?(retention)
253
+ now = singulus_multiton_monotonic_time
254
+ singulus_multiton_instances.delete_if { |_key, entry| entry.expires_at <= now }
255
+ elsif retention == :weak
256
+ singulus_multiton_instances.delete_if { |_key, entry| !entry.alive? }
257
+ end
258
+ end
259
+
260
+ def singulus_multiton_monotonic_time
261
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
262
+ end
263
+
264
+ def singulus_multiton_validate_retention_options!(strategy, ttl:, max_size:)
265
+ case strategy
266
+ when :forever, :weak
267
+ return unless ttl || max_size
268
+
269
+ raise Internal::InvalidRetentionError,
270
+ "#{strategy.inspect} retention does not accept ttl: or max_size:"
271
+ when :ttl
272
+ if !ttl || ttl.to_f <= 0
273
+ raise Internal::InvalidRetentionError,
274
+ ":ttl retention requires ttl: greater than zero"
275
+ end
276
+ return unless max_size
277
+
278
+ raise Internal::InvalidRetentionError,
279
+ "max_size: is not valid for :ttl retention; use retention: :bounded to combine ttl and max_size"
280
+ when :lru
281
+ if !max_size || max_size.to_i <= 0
282
+ raise Internal::InvalidRetentionError, ":lru retention requires max_size: greater than zero"
283
+ end
284
+ return unless ttl
285
+
286
+ raise Internal::InvalidRetentionError,
287
+ "ttl: is not valid for :lru retention; use retention: :bounded to combine ttl and max_size"
288
+ when :bounded
289
+ if !ttl || ttl.to_f <= 0
290
+ raise Internal::InvalidRetentionError,
291
+ ":bounded retention requires ttl: greater than zero"
292
+ end
293
+ return if max_size&.to_i&.positive?
294
+
295
+ raise Internal::InvalidRetentionError, ":bounded retention requires max_size: greater than zero"
296
+ end
297
+ end
298
+
299
+ def singulus_multiton_reject_recursive_initialization!(key)
300
+ return unless singulus_multiton_initializing_keys.key?(key)
301
+
302
+ raise Internal::RecursiveInitializationError,
303
+ "recursive Multiton initialization detected for key #{key.inspect} on #{self}"
304
+ end
305
+
306
+ def singulus_multiton_assert_registry_empty!(setting)
307
+ return if instance_count.zero?
308
+
309
+ raise Internal::ConfigurationLockedError,
310
+ "cannot change Multiton #{setting} after instances have been created; clear_instances first"
311
+ end
312
+ end
313
+
314
+ private_constant :ClassMethods
315
+ end
316
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Singulus
4
+ VERSION = "0.1.0"
5
+ end
data/lib/singulus.rb ADDED
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "monitor"
5
+ require "weakref"
6
+
7
+ require_relative "singulus/version"
8
+ require_relative "singulus/internal/errors"
9
+ require_relative "singulus/internal/configuration"
10
+ require_relative "singulus/internal/instance_guard"
11
+ require_relative "singulus/internal/constructor_guard"
12
+ require_relative "singulus/internal/reflection_guard"
13
+ require_relative "singulus/internal/mutation_guard"
14
+ require_relative "singulus/internal/singleton_class_methods"
15
+ require_relative "singulus/internal/runtime_hardening"
16
+ require_relative "singulus/multiton"
17
+
18
+ module Singulus
19
+ module Internal
20
+ CONSTRUCTORS = %i[new allocate].freeze
21
+
22
+ MARKER_IVAR = :@__singulus_managed_class__
23
+ KIND_IVAR = :@__singulus_kind__
24
+ MODE_IVAR = :@__singulus_mode__
25
+
26
+ class << self
27
+ def install_singleton!(base, mode: Singulus.configuration.default_mode)
28
+ base.include ::Singleton
29
+ base.include InstanceGuard
30
+
31
+ initialize_managed_class!(base, kind: :singleton, mode: mode)
32
+ base.instance_variable_set(:@__singulus_seal_mutex__, Thread::Mutex.new)
33
+ base.instance_variable_set(:@__singulus_sealed__, false)
34
+
35
+ base.singleton_class.prepend ReflectionGuard
36
+ base.singleton_class.prepend MutationGuard
37
+ base.singleton_class.prepend ConstructorGuard
38
+ base.extend SingletonClassMethods
39
+
40
+ RuntimeHardening.enable! if runtime_hardened?(base)
41
+ base
42
+ end
43
+
44
+ def initialize_managed_class!(base, kind:, mode:)
45
+ base.instance_variable_set(MARKER_IVAR, true)
46
+ base.instance_variable_set(KIND_IVAR, kind)
47
+ base.instance_variable_set(MODE_IVAR, normalize_mode!(mode))
48
+ base
49
+ end
50
+
51
+ def managed_class?(object)
52
+ object.is_a?(Class) && object.instance_variable_get(MARKER_IVAR) == true
53
+ end
54
+
55
+ alias singulus_class? managed_class?
56
+
57
+ def kind_for(klass)
58
+ return unless managed_class?(klass)
59
+
60
+ klass.instance_variable_get(KIND_IVAR)
61
+ end
62
+
63
+ def mode_for(klass)
64
+ return unless managed_class?(klass)
65
+
66
+ klass.instance_variable_get(MODE_IVAR)
67
+ end
68
+
69
+ def set_mode!(klass, mode)
70
+ raise ArgumentError, "#{klass} is not managed by Singulus" unless managed_class?(klass)
71
+
72
+ normalized_mode = normalize_mode!(mode)
73
+ current_mode = mode_for(klass)
74
+
75
+ if kind_for(klass) == :singleton && current_mode != :standard && normalized_mode == :standard &&
76
+ klass.instance_variable_get(:@__singulus_sealed__)
77
+ raise InvalidModeError,
78
+ "cannot downgrade #{klass} to :standard after its constructors have been sealed"
79
+ end
80
+
81
+ klass.instance_variable_set(MODE_IVAR, normalized_mode)
82
+ RuntimeHardening.enable! if normalized_mode == :runtime
83
+ normalized_mode
84
+ end
85
+
86
+ def locally_hardened?(klass)
87
+ %i[strict runtime].include?(mode_for(klass))
88
+ end
89
+
90
+ def runtime_hardened?(klass)
91
+ mode_for(klass) == :runtime
92
+ end
93
+
94
+ def sealed_singleton?(klass)
95
+ kind_for(klass) == :singleton && klass.instance_variable_get(:@__singulus_sealed__) == true
96
+ end
97
+
98
+ def constructor_name?(method_name)
99
+ CONSTRUCTORS.include?(normalize_method_name(method_name))
100
+ end
101
+
102
+ def normalize_method_name(method_name)
103
+ method_name.to_sym
104
+ rescue NoMethodError
105
+ method_name
106
+ end
107
+
108
+ def constructor_access_allowed?(klass)
109
+ permissions = Thread.current[:__singulus_constructor_permissions__]
110
+ permissions && permissions[klass].to_i.positive?
111
+ end
112
+
113
+ def with_constructor_access(klass)
114
+ permissions = (Thread.current[:__singulus_constructor_permissions__] ||= {})
115
+ permissions[klass] = permissions[klass].to_i + 1
116
+ yield
117
+ ensure
118
+ if permissions
119
+ permissions[klass] = permissions[klass].to_i - 1
120
+ permissions.delete(klass) unless permissions[klass].to_i.positive?
121
+ Thread.current[:__singulus_constructor_permissions__] = nil if permissions.empty?
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def normalize_mode!(mode)
128
+ normalized_mode = normalize_method_name(mode)
129
+ return normalized_mode if Configuration::MODES.include?(normalized_mode)
130
+
131
+ raise InvalidModeError,
132
+ "invalid Singulus mode #{mode.inspect}; expected one of: #{Configuration::MODES.join(', ')}"
133
+ end
134
+ end
135
+ end
136
+
137
+ private_constant :Internal
138
+
139
+ class << self
140
+ def configuration
141
+ @configuration ||= Internal::Configuration.new
142
+ end
143
+
144
+ def configure
145
+ raise ArgumentError, "a block is required" unless block_given?
146
+
147
+ yield configuration
148
+ Internal::RuntimeHardening.enable! if configuration.default_mode == :runtime
149
+ configuration
150
+ end
151
+
152
+ def reset_configuration!
153
+ @configuration = Internal::Configuration.new
154
+ end
155
+ end
156
+
157
+ module Singleton
158
+ class << self
159
+ def included(base)
160
+ Internal.install_singleton!(base)
161
+ end
162
+
163
+ def with(mode = nil, **options)
164
+ resolved_mode = options.delete(:mode)
165
+
166
+ raise ArgumentError, "mode must be provided either positionally or as mode:, not both" if mode && resolved_mode
167
+
168
+ unless options.empty?
169
+ raise ArgumentError, "unknown Singleton options: #{options.keys.map(&:inspect).join(', ')}"
170
+ end
171
+
172
+ resolved_mode ||= mode || Singulus.configuration.default_mode
173
+
174
+ Module.new do
175
+ define_singleton_method(:included) do |base|
176
+ Internal.install_singleton!(base, mode: resolved_mode)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: singulus
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
+ Singulus provides configurable local and runtime hardening for classic
14
+ Singleton and keyed Multiton patterns, including reflection guards, constructor
15
+ protection, mutation protection, inheritance restrictions, bounded retention
16
+ strategies, and optional guards around Method and UnboundMethod.
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ - lib/singulus.rb
25
+ - lib/singulus/internal/configuration.rb
26
+ - lib/singulus/internal/constructor_guard.rb
27
+ - lib/singulus/internal/errors.rb
28
+ - lib/singulus/internal/instance_guard.rb
29
+ - lib/singulus/internal/mutation_guard.rb
30
+ - lib/singulus/internal/reflection_guard.rb
31
+ - lib/singulus/internal/runtime_hardening.rb
32
+ - lib/singulus/internal/singleton_class_methods.rb
33
+ - lib/singulus/multiton.rb
34
+ - lib/singulus/version.rb
35
+ homepage: https://github.com/Rubcraft/singulus
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ allowed_push_host: https://rubygems.org
40
+ rubygems_mfa_required: 'true'
41
+ source_code_uri: https://github.com/Rubcraft/singulus
42
+ changelog_uri: https://github.com/Rubcraft/singulus/blob/main/CHANGELOG.md
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '3.2'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 4.0.16
58
+ specification_version: 4
59
+ summary: Hardened Singleton and keyed Multiton patterns for Ruby
60
+ test_files: []