containable 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c4612ffe8feecbd919a8286e151409ed2cbbb2f3efb55b66ee3957782b84bcfd
4
+ data.tar.gz: f1e571c55ca9d2220ddd6cef0aa3a51a51b6af8f70ce6b42ce3ddec857051014
5
+ SHA512:
6
+ metadata.gz: cb282e4356aef70209655372b29494f63131d3c3491b7b50f4421b0ac269b9e5504411bc8e1d9fe5ca2ea787404f59b8c8a74aee836a6276bf1a3958d64196fc
7
+ data.tar.gz: 58a76ad49b7b3ddc42f8002a378752d98b91e61520e0dc2caa6fb69405fb507684429b60299a8a9f65c461c0196fc7f20cb2b44f581308be18a037474933f561
checksums.yaml.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ ���K��+f�K^К��S����ϚG��ͅ���x�C�[8KM���&�֭����}�D����T���٩��p��#�H��N�y|�Co���ؔ(�t��u��O{��@H������0��f�\70���6>Mq-�ZJ4��z[a,B��H�=z;C 8�;n?�u4��('�d͈��_R���2n��p
2
+ �59��;eF��&q����%�+�E�}�y"�a܃\���$���r���J�53�`g��0[��L���v7�b�카K?[
data/LICENSE.adoc ADDED
@@ -0,0 +1,134 @@
1
+ = Hippocratic License
2
+
3
+ Version: 2.1.0.
4
+
5
+ Purpose. The purpose of this License is for the Licensor named above to
6
+ permit the Licensee (as defined below) broad permission, if consistent
7
+ with Human Rights Laws and Human Rights Principles (as each is defined
8
+ below), to use and work with the Software (as defined below) within the
9
+ full scope of Licensor’s copyright and patent rights, if any, in the
10
+ Software, while ensuring attribution and protecting the Licensor from
11
+ liability.
12
+
13
+ Permission and Conditions. The Licensor grants permission by this
14
+ license ("License"), free of charge, to the extent of Licensor’s
15
+ rights under applicable copyright and patent law, to any person or
16
+ entity (the "Licensee") obtaining a copy of this software and
17
+ associated documentation files (the "Software"), to do everything with
18
+ the Software that would otherwise infringe (i) the Licensor’s copyright
19
+ in the Software or (ii) any patent claims to the Software that the
20
+ Licensor can license or becomes able to license, subject to all of the
21
+ following terms and conditions:
22
+
23
+ * Acceptance. This License is automatically offered to every person and
24
+ entity subject to its terms and conditions. Licensee accepts this
25
+ License and agrees to its terms and conditions by taking any action with
26
+ the Software that, absent this License, would infringe any intellectual
27
+ property right held by Licensor.
28
+ * Notice. Licensee must ensure that everyone who gets a copy of any part
29
+ of this Software from Licensee, with or without changes, also receives
30
+ the License and the above copyright notice (and if included by the
31
+ Licensor, patent, trademark and attribution notice). Licensee must cause
32
+ any modified versions of the Software to carry prominent notices stating
33
+ that Licensee changed the Software. For clarity, although Licensee is
34
+ free to create modifications of the Software and distribute only the
35
+ modified portion created by Licensee with additional or different terms,
36
+ the portion of the Software not modified must be distributed pursuant to
37
+ this License. If anyone notifies Licensee in writing that Licensee has
38
+ not complied with this Notice section, Licensee can keep this License by
39
+ taking all practical steps to comply within 30 days after the notice. If
40
+ Licensee does not do so, Licensee’s License (and all rights licensed
41
+ hereunder) shall end immediately.
42
+ * Compliance with Human Rights Principles and Human Rights Laws.
43
+ [arabic]
44
+ . Human Rights Principles.
45
+ [loweralpha]
46
+ .. Licensee is advised to consult the articles of the United Nations
47
+ Universal Declaration of Human Rights and the United Nations Global
48
+ Compact that define recognized principles of international human rights
49
+ (the "Human Rights Principles"). Licensee shall use the Software in a
50
+ manner consistent with Human Rights Principles.
51
+ .. Unless the Licensor and Licensee agree otherwise, any dispute,
52
+ controversy, or claim arising out of or relating to (i) Section 1(a)
53
+ regarding Human Rights Principles, including the breach of Section 1(a),
54
+ termination of this License for breach of the Human Rights Principles,
55
+ or invalidity of Section 1(a) or (ii) a determination of whether any Law
56
+ is consistent or in conflict with Human Rights Principles pursuant to
57
+ Section 2, below, shall be settled by arbitration in accordance with the
58
+ Hague Rules on Business and Human Rights Arbitration (the "Rules");
59
+ provided, however, that Licensee may elect not to participate in such
60
+ arbitration, in which event this License (and all rights licensed
61
+ hereunder) shall end immediately. The number of arbitrators shall be one
62
+ unless the Rules require otherwise.
63
+ +
64
+ Unless both the Licensor and Licensee agree to the contrary: (1) All
65
+ documents and information concerning the arbitration shall be public and
66
+ may be disclosed by any party; (2) The repository referred to under
67
+ Article 43 of the Rules shall make available to the public in a timely
68
+ manner all documents concerning the arbitration which are communicated
69
+ to it, including all submissions of the parties, all evidence admitted
70
+ into the record of the proceedings, all transcripts or other recordings
71
+ of hearings and all orders, decisions and awards of the arbitral
72
+ tribunal, subject only to the arbitral tribunal’s powers to take such
73
+ measures as may be necessary to safeguard the integrity of the arbitral
74
+ process pursuant to Articles 18, 33, 41 and 42 of the Rules; and (3)
75
+ Article 26(6) of the Rules shall not apply.
76
+ . Human Rights Laws. The Software shall not be used by any person or
77
+ entity for any systems, activities, or other uses that violate any Human
78
+ Rights Laws. "Human Rights Laws" means any applicable laws,
79
+ regulations, or rules (collectively, "Laws") that protect human,
80
+ civil, labor, privacy, political, environmental, security, economic, due
81
+ process, or similar rights; provided, however, that such Laws are
82
+ consistent and not in conflict with Human Rights Principles (a dispute
83
+ over the consistency or a conflict between Laws and Human Rights
84
+ Principles shall be determined by arbitration as stated above). Where
85
+ the Human Rights Laws of more than one jurisdiction are applicable or in
86
+ conflict with respect to the use of the Software, the Human Rights Laws
87
+ that are most protective of the individuals or groups harmed shall
88
+ apply.
89
+ . Indemnity. Licensee shall hold harmless and indemnify Licensor (and
90
+ any other contributor) against all losses, damages, liabilities,
91
+ deficiencies, claims, actions, judgments, settlements, interest, awards,
92
+ penalties, fines, costs, or expenses of whatever kind, including
93
+ Licensor’s reasonable attorneys’ fees, arising out of or relating to
94
+ Licensee’s use of the Software in violation of Human Rights Laws or
95
+ Human Rights Principles.
96
+ * Failure to Comply. Any failure of Licensee to act according to the
97
+ terms and conditions of this License is both a breach of the License and
98
+ an infringement of the intellectual property rights of the Licensor
99
+ (subject to exceptions under Laws, e.g., fair use). In the event of a
100
+ breach or infringement, the terms and conditions of this License may be
101
+ enforced by Licensor under the Laws of any jurisdiction to which
102
+ Licensee is subject. Licensee also agrees that the Licensor may enforce
103
+ the terms and conditions of this License against Licensee through
104
+ specific performance (or similar remedy under Laws) to the extent
105
+ permitted by Laws. For clarity, except in the event of a breach of this
106
+ License, infringement, or as otherwise stated in this License, Licensor
107
+ may not terminate this License with Licensee.
108
+ * Enforceability and Interpretation. If any term or provision of this
109
+ License is determined to be invalid, illegal, or unenforceable by a
110
+ court of competent jurisdiction, then such invalidity, illegality, or
111
+ unenforceability shall not affect any other term or provision of this
112
+ License or invalidate or render unenforceable such term or provision in
113
+ any other jurisdiction; provided, however, subject to a court
114
+ modification pursuant to the immediately following sentence, if any term
115
+ or provision of this License pertaining to Human Rights Laws or Human
116
+ Rights Principles is deemed invalid, illegal, or unenforceable against
117
+ Licensee by a court of competent jurisdiction, all rights in the
118
+ Software granted to Licensee shall be deemed null and void as between
119
+ Licensor and Licensee. Upon a determination that any term or provision
120
+ is invalid, illegal, or unenforceable, to the extent permitted by Laws,
121
+ the court may modify this License to affect the original purpose that
122
+ the Software be used in compliance with Human Rights Principles and
123
+ Human Rights Laws as closely as possible. The language in this License
124
+ shall be interpreted as to its fair meaning and not strictly for or
125
+ against any party.
126
+ * Disclaimer. TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE COMES
127
+ "AS IS," WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR AND
128
+ ANY OTHER CONTRIBUTOR SHALL NOT BE LIABLE TO ANYONE FOR ANY DAMAGES OR
129
+ OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE
130
+ OR THIS LICENSE, UNDER ANY KIND OF LEGAL CLAIM.
131
+
132
+ This Hippocratic License is an link:https://ethicalsource.dev[Ethical Source license] and is offered
133
+ for use by licensors and licensees at their own risk, on an "AS IS" basis, and with no warranties
134
+ express or implied, to the maximum extent permitted by Laws.
data/README.adoc ADDED
@@ -0,0 +1,551 @@
1
+ :toc: macro
2
+ :toclevels: 5
3
+ :figure-caption!:
4
+
5
+ :dependency_injection_containers_link: link:https://alchemists.io/articles/dependency_injection_containers[Dependency Injection Containers]
6
+ :infusible_link: link:https://alchemists.io/projects/infusible[Infusible]
7
+ :rspec_link: link:https://rspec.info[RSpec]
8
+ :test_doubles_link: link:https://alchemists.io/articles/rspec_test_doubles[Test Doubles]
9
+
10
+ = Containable
11
+
12
+ This gem provides a thread-safe container for defining dependencies for reuse within your application. Coupled with the {infusible_link} gem, this powerful combination makes {dependency_injection_containers_link} simple to implement, test, and maintain.
13
+
14
+ toc::[]
15
+
16
+ == Features
17
+
18
+ * Provides a thread-safe dependency injection container.
19
+ * Encourages composition over inheritance.
20
+ * Includes test suite support so you can swap in {test_doubles_link} if desired.
21
+ * Compatible with {infusible_link}.
22
+
23
+ == Requirements
24
+
25
+ . link:https://www.ruby-lang.org[Ruby].
26
+ . A strong understanding of {dependency_injection_containers_link}.
27
+
28
+ == Setup
29
+
30
+ To install _with_ security, run:
31
+
32
+ [source,bash]
33
+ ----
34
+ # 💡 Skip this line if you already have the public certificate installed.
35
+ gem cert --add <(curl --compressed --location https://alchemists.io/projects/containable/gems.pem)
36
+ gem install containable --trust-policy HighSecurity
37
+ ----
38
+
39
+ To install _without_ security, run:
40
+
41
+ [source,bash]
42
+ ----
43
+ gem install containable
44
+ ----
45
+
46
+ You can also add the gem directly to your project:
47
+
48
+ [source,bash]
49
+ ----
50
+ bundle add containable
51
+ ----
52
+
53
+ Once the gem is installed, you only need to require it:
54
+
55
+ [source,ruby]
56
+ ----
57
+ require "containable"
58
+ ----
59
+
60
+ == Usage
61
+
62
+ You can immediately use this gem by creating a container, extending the container with functionality from this gem, and register any/all dependencies as desired. Example:
63
+
64
+ [source,ruby]
65
+ ----
66
+ require "containable"
67
+
68
+ module Container
69
+ extend Containable
70
+
71
+ register :literal, 1
72
+ register(:echo) { |text| text }
73
+ end
74
+
75
+ puts Container[:literal] # 1
76
+ puts Container[:echo].call "test" # "test"
77
+ ----
78
+
79
+ The rest of this section will expand upon what is shown above.
80
+
81
+ === Modules
82
+
83
+ Containers _must be modules_. For example, attempting to turn a class into a container is not allowed:
84
+
85
+ [source,ruby]
86
+ ----
87
+ require "containable"
88
+
89
+ class Container
90
+ extend Containable
91
+ end
92
+
93
+ # Only a module can be a container. (TypeError)
94
+ ----
95
+
96
+ This is important since containers are only meant to hold your dependencies and nothing else. Modules are the perfect construct for this.
97
+
98
+ === Registration
99
+
100
+ As shown above, the best way to register dependencies is as you define your container. The most basic is via a key/value pair:
101
+
102
+ [source,ruby]
103
+ ----
104
+ require "containable"
105
+
106
+ module Container
107
+ extend Containable
108
+
109
+ register :demo, 1
110
+ end
111
+ ----
112
+
113
+ With the above, `1` (literal) will be associated with the `:demo` key. This is perfect for registering literals, constants, or any objects you immediately want evaluated or have a reference to. To lazily register a dependency, use a block with parameters:
114
+
115
+ [source,ruby]
116
+ ----
117
+ require "containable"
118
+
119
+ module Container
120
+ extend Containable
121
+
122
+ register(:demo) { Object.new }
123
+ end
124
+ ----
125
+
126
+ In this case the `:demo` key is associated with an instance of an object but the instance _will only be realized when first resolved_. Until the `:demo` key is resolved, the object is not instantiated and remains a closure (more on resolving dependencies shortly). You can also register procs, lambdas, and functions in the same manner:
127
+
128
+ [source,ruby]
129
+ ----
130
+ require "containable"
131
+
132
+ function = proc { 3 }
133
+
134
+ module Container
135
+ extend Containable
136
+
137
+ register :one, proc { 1 }
138
+ register :two, -> { 2 }
139
+ register(:three, &function)
140
+ end
141
+ ----
142
+
143
+ As you can see, registration is quite flexible. That said, you only register either a value or closure but not both. For example, if you register both a value _and_ a closure you'll get a warning (as printed as standard error output):
144
+
145
+ [source,ruby]
146
+ ----
147
+ require "containable"
148
+
149
+ module Container
150
+ extend Containable
151
+
152
+ register(:demo, "bogus") { 1 }
153
+ end
154
+
155
+ # Registration of value is ignored since block takes precedence.
156
+ ----
157
+
158
+ While providing the value isn't harmful, it is unnecessary and wasteful. Instead, supply a value or a closure _but not both_.
159
+
160
+ You can also register dependencies after the fact since the container is open, by default. Example:
161
+
162
+ [source,ruby]
163
+ ----
164
+ require "containable"
165
+
166
+ module Container
167
+ extend Containable
168
+
169
+ register :one, 1
170
+ end
171
+
172
+ Container.register :two, 2
173
+ Container[:three] = 3
174
+ ----
175
+
176
+ With the above, a combination of `.register` and `.[]=` (setter) messages are used. While the latter is handy the former should be preferred for improved readability.
177
+
178
+ === Resolution
179
+
180
+ Now that you understand how to register dependencies, we can talk about resolving them. There are two ways to resolve a dependency. Example:
181
+
182
+ [source,ruby]
183
+ ----
184
+ Container[:demo]
185
+ Container.resolve(:demo)
186
+ ----
187
+
188
+ Both messages are acceptable but using `.[]` (getter) is recommended due to being succinct, requires less typing, and allows the container to feel more like a `Hash`. Internally, when resolving a dependency, all keys are stored as strings which means you can use symbols or strings interchangeably except when using namespaces (more on this shortly). Example:
189
+
190
+ [source,ruby]
191
+ ----
192
+ Container[:demo] # "example"
193
+ Container["demo"] # "example"
194
+ ----
195
+
196
+ When discussing registration earlier, we saw you can register values and closures. A value can also be a closure but if a block is registered -- in addition to the value -- the block takes precedence over the value.
197
+
198
+ What hasn't been discussed is the _kind_ of closure used when registering a value or block. If a closure takes _no parameters_, then the closure will be resolved immediately when resolving the key for the first time. Any closure that takes one more more parameters will never be resolved which means you can call the closure directly when needed. To illustrate, consider the following:
199
+
200
+ [source,ruby]
201
+ ----
202
+ require "containable"
203
+
204
+ module Container
205
+ extend Containable
206
+
207
+ register :one, proc { 1 }
208
+ register(:two) { |text| text.upcase }
209
+ register :three, -> text { text.reverse }
210
+ end
211
+
212
+ Container[:one] # 1
213
+ Container[:two] # #<Proc:0x000000012e9f8718 /demo:23>
214
+ Container[:three] # #<Proc:0x000000012e9f8628 /demo:24 (lambda)>
215
+ ----
216
+
217
+ With the above, you can see `:one` was immediately resolved to the value of `1` even though it was wrapped in a closure to begin with. This happened because the closure had no parameters so was safe to resolve. Again, this allows you to lazily resolve a dependency until you need it.
218
+
219
+ For keys `:two` and `:three`, we have a closure that has at least one parameter so remains a closure so you can supply the arguments you need later. Here's a closer look of using the `:two` and `:three` dependencies:
220
+
221
+ [source,ruby]
222
+ ----
223
+ Container[:two].call "demo" # "DEMO"
224
+ Container[:three].call "demo" # "omed"
225
+ ----
226
+
227
+ In all of these situations, we have closures supplied as values or blocks but only closures with out parameters are resolved (i.e. unwrapped).
228
+
229
+ === Namespaces
230
+
231
+ As hinted at earlier, you can namespace your dependencies for improved organization. Example:
232
+
233
+ [source,ruby]
234
+ ----
235
+ require "containable"
236
+
237
+ module Container
238
+ extend Containable
239
+
240
+ namespace :one do
241
+ register :blue, "blue"
242
+ end
243
+
244
+ namespace :two do
245
+ register :green, "green"
246
+ end
247
+
248
+ namespace "three" do
249
+ register :grey, "grey"
250
+ register :silver, "silver"
251
+ end
252
+ end
253
+ ----
254
+
255
+ There is no limit on the number of namespaces used or how deep they are nested. That said, this functionality _should not be abused_ by sticking to either one or two levels of hierarchy. Anything more than that and you should reflect if your implementation is overly complex in order to refactor accordingly.
256
+
257
+ As with registration, you can use symbols, strings, or both for your namespaces since they are stored internally as strings. Namespaces are delimited by periods (`.`) so you _must_ use a string for your key to resolve them. Example:
258
+
259
+ [source,ruby]
260
+ ----
261
+ Container["one.blue"] # "blue"
262
+ Container["two.green"] # "green"
263
+ Container["three.silver"] # "silver"
264
+ ----
265
+
266
+ === Enumeration
267
+
268
+ Limited enumeration of your container is possible. Given the following:
269
+
270
+ [source,ruby]
271
+ ----
272
+ require "containable"
273
+
274
+ module Container
275
+ extend Containable
276
+
277
+ register :one, 1
278
+ register :two, 2
279
+ end
280
+ ----
281
+
282
+ ...this means you can use all of the following messages:
283
+
284
+ [source,ruby]
285
+ ----
286
+ Container.each { |key, value| puts "#{key}=#{value}" }
287
+ # one=1
288
+ # two=2
289
+
290
+ Container.each_key { |key| puts "Key: #{key}" }
291
+ # Key: one
292
+ # Key: two
293
+
294
+ Container.key? :one # false
295
+ Container.key? "one" # true
296
+
297
+ Container.keys # ["one", "two"]
298
+ ----
299
+
300
+ === Freezing
301
+
302
+ You can freeze your container and immediately check if it is frozen. Example:
303
+
304
+ [source,ruby]
305
+ ----
306
+ require "containable"
307
+
308
+ module Container
309
+ extend Containable
310
+
311
+ register :demo, "An example."
312
+ freeze
313
+ end
314
+
315
+ Container.frozen? # true
316
+ ----
317
+
318
+ You can also freeze your container after the fact by messaging `.freeze` directly on the container: `Container.freeze`. Once a container if frozen, registration of additional dependencies will result in an error:
319
+
320
+ [source,]
321
+ ----
322
+ Container.register :another, "One more."
323
+ # Can't modify frozen container. (FrozenError)
324
+ ----
325
+
326
+ Once frozen, the container can't be unfrozen unless you duplicate it (see below).
327
+
328
+ === Duplicates
329
+
330
+ You can duplicate a container via the following (which will unfreeze the container if previously frozen):
331
+
332
+ [source,ruby]
333
+ ----
334
+ container = Container.dup
335
+ container.name
336
+ # "module:container"
337
+
338
+ Other = Container.dup
339
+ Other.name
340
+ # "Other"
341
+ ----
342
+
343
+ As you can see a container, once duplicated, can be assigned to a local variable or a new constant. When assigning to a variable, the container will use a temporary name of `module:container` to help identify it.
344
+
345
+ === Clones
346
+
347
+ Cloning a container is identical to duplicating a container _except_ if the container is frozen then the clone will be frozen too. Example:
348
+
349
+ [source,ruby]
350
+ ----
351
+ Container.freeze
352
+ Container.clone.frozen? # true
353
+ ----
354
+
355
+ === Customization
356
+
357
+ You can customize how the container registers and resolves dependencies by creating your own register and resolver objects. For example, here's how to use a custom register that doesn't care if you override an existing key.
358
+
359
+ [source,ruby]
360
+ ----
361
+ require "containable"
362
+
363
+ class CustomRegister < Containable::Register
364
+ def call(key, value = nil, &block) = dependencies[namespacify(key)] = block || value
365
+ end
366
+
367
+ module Container
368
+ extend Containable[register: CustomRegister]
369
+
370
+ register :one, 1
371
+ register :one, "override"
372
+ end
373
+
374
+ Container[:one] # "override"
375
+ ----
376
+
377
+ ...and here's an example with a custom resolver that only allows specific keys to be resolved:
378
+
379
+ [source,ruby]
380
+ ----
381
+ require "containable"
382
+
383
+ class CustomResolver < Containable::Resolver
384
+ def initialize *, allowed_keys: %i[one three]
385
+ super(*)
386
+ @allowed_keys = allowed_keys
387
+ end
388
+
389
+ def call key
390
+ fail KeyError, "Only use these keys: #{allowed_keys.inspect}" unless allowed_keys.include? key
391
+
392
+ super
393
+ end
394
+
395
+ private
396
+
397
+ attr_reader :allowed_keys
398
+ end
399
+
400
+ module Container
401
+ extend Containable[resolver: CustomResolver]
402
+
403
+ register :one, 1
404
+ register :two, 2
405
+ register :three, 3
406
+ end
407
+
408
+ Container[:one] # 1
409
+ Container[:two] # Only use these keys: [:one, :three] (KeyError)
410
+ Container[:three] # 3
411
+ ----
412
+
413
+ In both cases, you only need to inject your custom register or resolver when extending your container with `Containable`. Both of these classes should inherit from either `Containable::Register` or `Containable::Resolver` to customize behavior as you like. Definitely check out the source code of both these classes to learn more and customize as desired.
414
+
415
+ === Infusible
416
+
417
+ To fully leverage the power of this gem, check out {infusible_link}. You can get far with simple containers but if you want to supercharge your containers and make your architecture truly come alive then make sure to couple this gem with the {infusible_link} gem. 🚀
418
+
419
+ === Tests
420
+
421
+ As you architect your implementation, you'll want to swap out your original dependencies with {test_doubles_link} to simplify testing especially for situations, like making HTTP requests, with a fake. For demonstration purposes, I'll assume you are using {rspec_link} but you can adapt for whatever testing framework you are using.
422
+
423
+ Consider the following:
424
+
425
+ [source,ruby]
426
+ ----
427
+ module Container
428
+ extend Containable
429
+
430
+ register :kernel, Kernel
431
+ end
432
+
433
+ class Demo
434
+ def initialize container: Container
435
+ @container = container
436
+ end
437
+
438
+ def speak(text) = kernel.puts text
439
+
440
+ private
441
+
442
+ attr_reader :container
443
+
444
+ def kernel = container[__method__]
445
+ end
446
+ ----
447
+
448
+ With our implementation defined, we can test as follows:
449
+
450
+ [source,ruby]
451
+ ----
452
+ RSpec.describe Demo do
453
+ subject(:demo) { Demo.new }
454
+
455
+ let(:kernel) { class_spy Kernel }
456
+
457
+ before { Container.stub! kernel: }
458
+ after { Container.restore }
459
+
460
+ describe "#call" do
461
+ it "prints message" do
462
+ demo.speak "Hello"
463
+ expect(kernel).to have_received(:puts).with("Hello")
464
+ end
465
+ end
466
+ end
467
+ ----
468
+
469
+ Notice there is little setup required to test the injected dependencies. Simply define what you want stubbed in your `before` and `after` blocks. That's it!
470
+
471
+ While the above works great for a single spec, over time you'll want to reduce duplicated setup by using a shared context. Here's a rewrite of the above spec which significantly reduces duplication when needing to test multiple objects using the same dependencies:
472
+
473
+ [source,ruby]
474
+ ----
475
+ # spec/support/shared_contexts/application_container.rb
476
+
477
+ RSpec.shared_context "with application dependencies" do
478
+ let(:kernel) { class_spy Kernel }
479
+
480
+ before { Container.stub! kernel: }
481
+ after { Container.restore }
482
+ end
483
+ ----
484
+
485
+ [source,ruby]
486
+ ----
487
+ # spec/lib/demo_spec.rb
488
+
489
+ RSpec.describe Demo do
490
+ subject(:demo) { Demo.new }
491
+
492
+ include_context "with application dependencies"
493
+
494
+ describe "#call" do
495
+ it "prints message" do
496
+ demo.speak "Hello"
497
+ expect(kernel).to have_received(:puts).with("Hello")
498
+ end
499
+ end
500
+ end
501
+ ----
502
+
503
+ You'll notice, in all of the examples, only two methods are used: `.stub!` and `.restore`. The first allows you supply keyword arguments of all dependencies you want stubbed. The last ensures your test suite is properly cleaned up so all stubs are removed and the container is restored to it's original state. If you don't restore your container after each spec, you'll end up with stubs leaking across your specs and {rspec_link} will error to the same effect as well.
504
+
505
+ _Always_ use `.stub!` to set your container up for testing. Once setup, you can add more stubs by using the `.stub` method (without the bang). So, to recap, use `.stub!` as a one-liner for setup and initial stubs then use `.stub` to add more stubs after the fact. Finally, ensure you restore (i.e. `.restore`) your container for proper cleanup after each test.
506
+
507
+ ‼️ Use of `.stub!`, while convenient for testing, should -- under no circmstances -- be used in production code because it is meant for testing purposes only.
508
+
509
+ == Development
510
+
511
+ To contribute, run:
512
+
513
+ [source,bash]
514
+ ----
515
+ git clone https://github.com/bkuhlmann/containable
516
+ cd containable
517
+ bin/setup
518
+ ----
519
+
520
+ You can also use the IRB console for direct access to all objects:
521
+
522
+ [source,bash]
523
+ ----
524
+ bin/console
525
+ ----
526
+
527
+ == Tests
528
+
529
+ To test, run:
530
+
531
+ [source,bash]
532
+ ----
533
+ bin/rake
534
+ ----
535
+
536
+ == link:https://alchemists.io/policies/license[License]
537
+
538
+ == link:https://alchemists.io/policies/security[Security]
539
+
540
+ == link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]
541
+
542
+ == link:https://alchemists.io/policies/contributions[Contributions]
543
+
544
+ == link:https://alchemists.io/projects/containable/versions[Versions]
545
+
546
+ == link:https://alchemists.io/community[Community]
547
+
548
+ == Credits
549
+
550
+ * Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].
551
+ * Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "containable"
5
+ spec.version = "0.0.0"
6
+ spec.authors = ["Brooke Kuhlmann"]
7
+ spec.email = ["brooke@alchemists.io"]
8
+ spec.homepage = "https://alchemists.io/projects/containable"
9
+ spec.summary = "A thread-safe container for use with dependency injection."
10
+ spec.license = "Hippocratic-2.1"
11
+
12
+ spec.metadata = {
13
+ "bug_tracker_uri" => "https://github.com/bkuhlmann/containable/issues",
14
+ "changelog_uri" => "https://alchemists.io/projects/containable/versions",
15
+ "documentation_uri" => "https://alchemists.io/projects/containable",
16
+ "funding_uri" => "https://github.com/sponsors/bkuhlmann",
17
+ "label" => "Containable",
18
+ "rubygems_mfa_required" => "true",
19
+ "source_code_uri" => "https://github.com/bkuhlmann/containable"
20
+ }
21
+
22
+ spec.signing_key = Gem.default_key_path
23
+ spec.cert_chain = [Gem.default_cert_path]
24
+
25
+ spec.required_ruby_version = "~> 3.3"
26
+ spec.add_dependency "concurrent-ruby", "~> 1.2"
27
+
28
+ spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
29
+ spec.files = Dir["*.gemspec", "lib/**/*"]
30
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/hash"
4
+
5
+ module Containable
6
+ # Registers dependencies for future evaluation.
7
+ class Register
8
+ SEPARATOR = "."
9
+
10
+ def initialize dependencies = Concurrent::Hash.new, separator: SEPARATOR
11
+ @dependencies = dependencies
12
+ @separator = separator
13
+ @keys = []
14
+ @depth = 0
15
+ end
16
+
17
+ def call key, value = nil, &block
18
+ namespaced_key = namespacify key
19
+ message = "Dependency is already registered: #{key.inspect}."
20
+
21
+ warn "Registration of value is ignored since block takes precedence." if value && block
22
+ fail KeyError, message if dependencies.key? namespaced_key
23
+
24
+ dependencies[namespaced_key] = block || value
25
+ end
26
+
27
+ alias register call
28
+
29
+ def namespace(name, &)
30
+ keys.clear if depth.zero?
31
+ keys.append name
32
+ visit(&)
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :dependencies, :separator
38
+
39
+ attr_accessor :keys, :depth
40
+
41
+ def visit &block
42
+ increment
43
+ instance_eval(&block) if block
44
+ keys.pop
45
+ decrement
46
+ end
47
+
48
+ def increment = self.depth += 1
49
+
50
+ def decrement = self.depth -= 1
51
+
52
+ def namespacify(key) = keys[..depth].append(key).join separator
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/hash"
4
+
5
+ module Containable
6
+ # Resolves previously registered dependencies.
7
+ class Resolver
8
+ def initialize dependencies = Concurrent::Hash.new
9
+ @dependencies = dependencies
10
+ end
11
+
12
+ def call key
13
+ normalized_key = key.to_s
14
+
15
+ value = dependencies.fetch normalized_key do
16
+ fail KeyError, "Unable to resolve dependency: #{key.inspect}."
17
+ end
18
+
19
+ value.is_a?(Proc) && value.arity.zero? ? dependencies[normalized_key] = value.call : value
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :dependencies
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Containable
4
+ # Allows stubbing of dependencies for testing purposes only.
5
+ module Test
6
+ def resolve(key) = stubs.fetch(key.to_s) { super(key) }
7
+
8
+ alias [] resolve
9
+
10
+ def stub(**overrides)
11
+ @originals ||= dependencies.dup
12
+
13
+ overrides.each do |key, value|
14
+ normalized_key = key.to_s
15
+
16
+ fail KeyError, "Unable to stub unknown key: #{key.inspect}." unless key? normalized_key
17
+
18
+ stubs[normalized_key] = value
19
+ end
20
+ end
21
+
22
+ def stub!(**) = stub(**)
23
+
24
+ def restore
25
+ stubs.clear
26
+ dependencies.replace originals if originals
27
+ true
28
+ end
29
+
30
+ private
31
+
32
+ def originals = @originals
33
+
34
+ def stubs = @stubs ||= {}
35
+ end
36
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/hash"
4
+
5
+ module Containable
6
+ # Provides safe registration and resolution of dependencies.
7
+ class Vault < Module
8
+ def initialize dependencies = Concurrent::Hash.new, register: Register, resolver: Resolver
9
+ super()
10
+
11
+ @dependencies = dependencies
12
+ @register = register.new dependencies
13
+ @resolver = resolver.new dependencies
14
+
15
+ private_methods.grep(/\A(define)_/).sort.each { |method| __send__ method }
16
+
17
+ alias_method :[]=, :register
18
+ alias_method :[], :resolve
19
+ end
20
+
21
+ def extended descendant
22
+ fail TypeError, "Only a module can be a container." if descendant.is_a? Class
23
+
24
+ super
25
+ descendant.class_eval "private_class_method :dependencies", __FILE__, __LINE__
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :dependencies, :register, :resolver
31
+
32
+ def define_dependencies target = dependencies
33
+ define_method(:dependencies) { target }
34
+ end
35
+
36
+ def define_register target = register
37
+ define_method :register do |key, value = nil, &block|
38
+ fail FrozenError, "Can't modify frozen container." if dependencies.frozen?
39
+
40
+ target.call key, value, &block
41
+ end
42
+ end
43
+
44
+ def define_namespace target = register
45
+ define_method(:namespace) { |name, &block| target.namespace name, &block }
46
+ end
47
+
48
+ def define_resolve target = resolver
49
+ define_method(:resolve) { |key| target.call key }
50
+ end
51
+
52
+ def define_each target = dependencies
53
+ define_method(:each) { |&block| target.each(&block) }
54
+ end
55
+
56
+ def define_each_key target = dependencies
57
+ define_method(:each_key) { |&block| target.each_key(&block) }
58
+ end
59
+
60
+ def define_key? target = dependencies
61
+ define_method(:key?) { |name| target.key? name }
62
+ end
63
+
64
+ def define_keys target = dependencies
65
+ define_method(:keys) { target.keys }
66
+ end
67
+
68
+ def define_clone
69
+ define_method :clone do
70
+ dup.tap { |duplicate| duplicate.freeze if dependencies.frozen? }
71
+ end
72
+ end
73
+
74
+ def define_dup target = self.class,
75
+ local_register: register.class,
76
+ local_resolver: resolver.class
77
+
78
+ define_method :dup do
79
+ instance = target.new dependencies.dup, register: local_register, resolver: local_resolver
80
+ Module.new.set_temporary_name("module:container").extend instance
81
+ end
82
+ end
83
+
84
+ def define_freeze
85
+ define_method(:freeze) { dependencies.freeze and self }
86
+ end
87
+
88
+ def define_frozen?
89
+ define_method(:frozen?) { dependencies.frozen? }
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "containable/register"
4
+ require "containable/resolver"
5
+ require "containable/vault"
6
+
7
+ # Main namespace.
8
+ module Containable
9
+ def self.extended descendant
10
+ super
11
+ descendant.extend Vault.new
12
+ end
13
+
14
+ def self.[](register: Register, resolver: Resolver) = Vault.new(register:, resolver:)
15
+
16
+ def stub!(**)
17
+ require "containable/test"
18
+ extend Test
19
+ stub(**)
20
+ end
21
+ end
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: containable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brooke Kuhlmann
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIEeDCCAuCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZicm9v
14
+ a2UxGjAYBgoJkiaJk/IsZAEZFgphbGNoZW1pc3RzMRIwEAYKCZImiZPyLGQBGRYC
15
+ aW8wHhcNMjMwMzIyMTYxNDQxWhcNMjUwMzIxMTYxNDQxWjBBMQ8wDQYDVQQDDAZi
16
+ cm9va2UxGjAYBgoJkiaJk/IsZAEZFgphbGNoZW1pc3RzMRIwEAYKCZImiZPyLGQB
17
+ GRYCaW8wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCro8tj5/E1Hg88
18
+ f4qfiwPVd2zJQHvdYt4GHVvuHRRgx4HGhJuNp+4BId08RBn7V6V1MW6MY3kezRBs
19
+ M+7QOQ4b1xNLTvY7FYQB1wGK5a4x7TTokDrPYQxDB2jmsdDYCzVbIMrAvUfcecRi
20
+ khyGZCdByiiCl4fKv77P12tTT+NfsvXkLt/AYCGwjOUyGKTQ01Z6eC09T27GayPH
21
+ QQvIkakyFgcJtzSyGzs8bzK5q9u7wQ12MNTjJoXzW69lqp0oNvDylu81EiSUb5S6
22
+ QzzPxZBiRB1sgtbt1gUbVI262ZDq1gR+HxPFmp+Cgt7ZLIJZAtesQvtcMzseXpfn
23
+ hpmm0Sw22KGhRAy/mqHBRhDl5HqS1SJp2Ko3lcnpXeFResp0HNlt8NSu13vhC08j
24
+ GUHU9MyIXbFOsnp3K3ADrAVjPWop8EZkmUR3MV/CUm00w2cZHCSGiXl1KMpiVKvk
25
+ Ywr1gd2ZME4QLSo+EXUtLxDUa/W3xnBS8dBOuMMz02FPWYr3PN8CAwEAAaN7MHkw
26
+ CQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFAFgmv0tYMZnItuPycSM
27
+ F5wykJEVMB8GA1UdEQQYMBaBFGJyb29rZUBhbGNoZW1pc3RzLmlvMB8GA1UdEgQY
28
+ MBaBFGJyb29rZUBhbGNoZW1pc3RzLmlvMA0GCSqGSIb3DQEBCwUAA4IBgQAX+EGY
29
+ 9RLYGxF1VLZz+G1ACQc4uyrCB6kXwI06kzUa5dF9tPXqTX9ffnz3/W8ck2IQhKzu
30
+ MKO2FVijzbDWTsZeZGglS4E+4Jxpau1lU9HhOIcKolv6LeC6UdALTFudY+GLb8Xw
31
+ REXgaJkjzzhkUSILmEnRwEbY08dVSl7ZAaxVI679vfI2yapLlIwpbBgmQTiTvPr3
32
+ qyyLUno9flYEOv9fmGHunSrM+gE0/0niGTXa5GgXBXYGS2he4LQGgSBfGp/cTwMU
33
+ rDKJRcusZ12lNBeDfgqACz/BBJF8FLodgk6rGMRZz7+ZmjjHEmpG5bQpR6Q2BuWL
34
+ XMtYk/QzaWuhiR7pWjiF8jbdd7RO6or0ohq7iFkokz/5xrtQ/vPzU2RQ3Qc6YaKw
35
+ 3n5C8/6Zh9DYTkpcwPSuIfAga6wf4nXc9m6JAw8AuMLaiWN/r/2s4zJsUHYERJEu
36
+ gZGm4JqtuSg8pYjPeIJxS960owq+SfuC+jxqmRA54BisFCv/0VOJi7tiJVY=
37
+ -----END CERTIFICATE-----
38
+ date: 2024-04-03 00:00:00.000000000 Z
39
+ dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: concurrent-ruby
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.2'
54
+ description:
55
+ email:
56
+ - brooke@alchemists.io
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files:
60
+ - README.adoc
61
+ - LICENSE.adoc
62
+ files:
63
+ - LICENSE.adoc
64
+ - README.adoc
65
+ - containable.gemspec
66
+ - lib/containable.rb
67
+ - lib/containable/register.rb
68
+ - lib/containable/resolver.rb
69
+ - lib/containable/test.rb
70
+ - lib/containable/vault.rb
71
+ homepage: https://alchemists.io/projects/containable
72
+ licenses:
73
+ - Hippocratic-2.1
74
+ metadata:
75
+ bug_tracker_uri: https://github.com/bkuhlmann/containable/issues
76
+ changelog_uri: https://alchemists.io/projects/containable/versions
77
+ documentation_uri: https://alchemists.io/projects/containable
78
+ funding_uri: https://github.com/sponsors/bkuhlmann
79
+ label: Containable
80
+ rubygems_mfa_required: 'true'
81
+ source_code_uri: https://github.com/bkuhlmann/containable
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '3.3'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.5.7
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: A thread-safe container for use with dependency injection.
101
+ test_files: []
metadata.gz.sig ADDED
Binary file