infusible 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: 543c2fdc713060365fa86371145a08208b9cd2ad206ec02cd5307806a8991019
4
+ data.tar.gz: 80d73a65ef4331bf57101dee0c3b0b60dfc6712e619fa91e943a6e51b851d171
5
+ SHA512:
6
+ metadata.gz: 70104e261ff6722e591c442684e197663c67e30a9cf164d4a3b58d2418a8271fbeb11ecb010ef8a65efcedf9e2f2b651288db2e10029e3a94857f942ac58f781
7
+ data.tar.gz: 9788f80e1bf860075eff3e12aa3168cb3481118976c8726bc82f7942563cde9a8b35e0ec836ad5eaa81d1764582d70ad348b44641a284e7e9df81e10a9c6d9fb
checksums.yaml.gz.sig ADDED
Binary file
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,471 @@
1
+ :toc: macro
2
+ :toclevels: 5
3
+ :figure-caption!:
4
+
5
+ :dry-auto_inject_link: link:https://dry-rb.org/gems/dry-auto_inject[Dry AutoInject]
6
+ :dry-container_link: link:https://dry-rb.org/gems/dry-container[Dry Container]
7
+ :http_link: link:https://github.com/httprb/http[HTTP]
8
+
9
+ = Infusible
10
+
11
+ Automatically infuses dependencies within your object through the use of advanced dependency injection. Dependency injection -- the _D_ in _SOLID_ design -- is a powerful way to compose complex architectures from small objects which have a single responsibility -- the _S_ in _SOLID_ design.
12
+
13
+ This gem is inspired by the {dry-auto_inject_link} gem. There are a few major differences between this gem and the original Dry AutoInject gem which are:
14
+
15
+ * All injected dependencies are _private by default_ in order to not break encapsulation and a _major_ reason why this gem was created.
16
+ * Uses keyword arguments only while the original Dry AutoInject gem supports positionals or hash arguments in addition to keyword arguments.
17
+
18
+ The entire architecture centers on the injection of a _container_ of dependencies. A container can be any object that responds to the `#[]` message and pairs well with the {dry-container_link} gem but a primitive `Hash` works too. Here's a quick example of Infusible in action:
19
+
20
+ [source,ruby]
21
+ ----
22
+ Import = Infusible.with({a: 1, b: 2, c: 3})
23
+
24
+ class Demo
25
+ include Import[:a, :b, :c]
26
+
27
+ def to_s = "My injected dependencies are: #{a}, #{b}, and #{c}."
28
+ end
29
+
30
+ puts Demo.new # My injected dependencies are: 1, 2, and 3.
31
+ ----
32
+
33
+ By using _infusing_ dependencies into your object, you have the ability to define common dependencies that can be injected without having to do the manual setup normally required to define a constructor and set private instance variables.
34
+
35
+ toc::[]
36
+
37
+ == Features
38
+
39
+ * Ensures injected dependencies are _private by default_.
40
+ * Uses a slimmed down architecture with a strong focus on keyword arguments.
41
+ * Built on top of the link:https://www.alchemists.io/projects/marameters[Marameters] gem.
42
+
43
+ == Requirements
44
+
45
+ . link:https://www.ruby-lang.org[Ruby].
46
+ . Knowledge of SOLID design principles.
47
+
48
+ == Setup
49
+
50
+ To install, run:
51
+
52
+ [source,bash]
53
+ ----
54
+ gem install infusible
55
+ ----
56
+
57
+ Add the following to your Gemfile file:
58
+
59
+ [source,ruby]
60
+ ----
61
+ gem "infusible"
62
+ ----
63
+
64
+ == Usage
65
+
66
+ There is basic and advanced usage. We'll start with basic and work our to advanced usage.
67
+
68
+ === Basic
69
+
70
+ This gem requires three steps for proper use:
71
+
72
+ . A container.
73
+ . An import constant.
74
+ . A class and/or multiple classes for dependencies to be injected into.
75
+
76
+ Let's walk through each staring by defining a container of dependencies.
77
+
78
+ ==== Containers
79
+
80
+ A container provides a common object for which you can group related dependencies for injection and reuse. {dry-container_link} is recommended for defining your dependencies but a primitive `Hash` or any object which responds to the `#[]` message works too.
81
+
82
+ For documentation purposes, the {dry-container_link} gem will be used. The following creates a simple container where you might want to use the {http_link} gem to make HTTP requests and log information using Ruby's native logger.
83
+
84
+ [source,ruby]
85
+ ----
86
+ require "http"
87
+ require "logger"
88
+
89
+ module Container
90
+ extend Dry::Container::Mixin
91
+
92
+ register(:http) { HTTP }
93
+ register(:logger) { Logger.new STDOUT }
94
+ end
95
+ ----
96
+
97
+ ==== Injectors
98
+
99
+ Once your container is defined, you'll want to define the corresponding injector for reuse within your application. Defining an injector only requires two lines of code:
100
+
101
+ [source,ruby]
102
+ ----
103
+ require "infusible"
104
+
105
+ Import = Infusible.with Container
106
+ ----
107
+
108
+ ==== Dependencies
109
+
110
+ With your container and import defined, you can inject your dependencies by including what you need:
111
+
112
+ [source,ruby]
113
+ ----
114
+ class Pinger
115
+ include Import[:http, :logger]
116
+
117
+ def call url
118
+ http.get(url).status.then { |status| logger.info %(The status of "#{url}" is #{status}.) }
119
+ end
120
+ end
121
+ ----
122
+
123
+ Now when you ping a URL, you'll see the status of the server logged to console using all injected dependencies:
124
+
125
+ [source,ruby]
126
+ ----
127
+ Pinger.new.call "https://duckduckgo.com"
128
+ # I, [2022-03-01T10:00:00.979741 #81819] INFO -- : The status of "https://duckduckgo.com" is 200 OK.
129
+ ----
130
+
131
+ === Advanced
132
+
133
+ When injecting your dependencies you _must_ always define what dependencies you want to require. By default, none will be injected. The following will demonstrate multiple ways in which to manage the injection of your dependencies.
134
+
135
+ ==== Keys
136
+
137
+ You can use symbols, strings, or a combination of both when defining which dependencies you want to inject. Example:
138
+
139
+ [source,ruby]
140
+ ----
141
+ class Pinger
142
+ include Import[:http, "logger"]
143
+
144
+ def call = puts "Using: #{http.inspect} and #{logger.inspect}."
145
+ end
146
+ ----
147
+
148
+ ==== Namespaces
149
+
150
+ To access namespaced dependencies within a container, you only need to provide the fully qualified path. Example:
151
+
152
+ [source,ruby]
153
+ ----
154
+ class Pinger
155
+ include Import["primary.http", "primary.logger"]
156
+
157
+ def call = puts "Using: #{http.inspect} and #{logger.inspect}."
158
+ end
159
+ ----
160
+
161
+ The namespace _and_ delimiter (i.e. `primary.`) will be removed so only `http` and `logger` are defined for use (as shown in the `#call` method).
162
+
163
+ ==== Aliases
164
+
165
+ Should you want to rename your namespaced dependencies to something more appropriate for your class, use a hash. Example:
166
+
167
+ [source,ruby]
168
+ ----
169
+ class Pinger
170
+ include Import[client: "primary.http"]
171
+
172
+ def call = puts "Using: #{client.inspect}."
173
+ end
174
+ ----
175
+
176
+ The aliased `"primary.http"` will be defined as `client` when imported (as shown in the `#call` method).
177
+
178
+ You can also mix names, namespaces, and aliases for injection as long as the aliases are defined last. Example:
179
+
180
+ [source,ruby]
181
+ ----
182
+ class Pinger
183
+ include Import[:configuration, "primary.logger", client: :http]
184
+
185
+ def call = puts "Using: #{configuration.inspect}, #{logger.inspect}, and #{client.inspect}."
186
+ end
187
+ ----
188
+
189
+ ==== Explicit Dependencies
190
+
191
+ Earlier, when demonstrating basic usage, all dependencies were injected by default:
192
+
193
+ [source,ruby]
194
+ ----
195
+ class Pinger
196
+ include Import[:http, :logger]
197
+ end
198
+ ----
199
+
200
+ ...but we could have a different class -- like a downloader -- that only needs the HTTP client. In that case, we could import the _same_ container but only require the HTTP dependency. Example:
201
+
202
+ [source,ruby]
203
+ ----
204
+ class Downloader
205
+ include Import[:http]
206
+ end
207
+ ----
208
+
209
+ This allows you to reuse your importer (i.e. `Import`) in as many situations as makes sense while improving performance.
210
+
211
+ ==== Custom Initialization
212
+
213
+ Should you want to use injection in combination with your own initializer, you'll need to ensure the injected dependencies are passed upward. All you need to do is define the injected dependencies as your last argument and then pass them to `super`. Example:
214
+
215
+ [source,ruby]
216
+ ----
217
+ class Pinger
218
+ include Import[:logger]
219
+
220
+ def initialize http: HTTP, **dependencies
221
+ super(**dependencies)
222
+
223
+ @http = http
224
+ end
225
+
226
+ private
227
+
228
+ attr_reader :http
229
+ end
230
+ ----
231
+
232
+ The above will ensure the logger gets passed upwards for _infusion_ and is accessible to your class as an HTTP dependency.
233
+
234
+ ==== Inheritance
235
+
236
+ When using inheritance or multiple inheritance, the child class' dependencies will take precedence over the parent's dependencies as long as the keys are the same. Consider the following:
237
+
238
+ [source,ruby]
239
+ ----
240
+ class Parent
241
+ def initialize logger: Logger.new(StringIO.new)
242
+ @logger = logger
243
+ end
244
+
245
+ private
246
+
247
+ attr_reader :logger
248
+ end
249
+
250
+ class Child < Parent
251
+ include Import[:logger]
252
+ end
253
+ ----
254
+
255
+ In the above situation, the child's logger will be the logger that is injected which overrides the default logger defined by the parent. This applies to multiple inheritance too. Example:
256
+
257
+ [source,ruby]
258
+ ----
259
+ class Parent
260
+ include GeneralImport[:logger]
261
+ end
262
+
263
+ class Child < Parent
264
+ include Import[:logger]
265
+ end
266
+ ----
267
+
268
+ Once again, the child's logger will take precedence over the what is provided by default by the parent. This also applies to multiple levels of inheritance or multiple inherited modules. Whichever is last, wins. Lastly, you can mix and match dependencies too:
269
+
270
+ [source,ruby]
271
+ ----
272
+ class Parent
273
+ include Import[:logger]
274
+ end
275
+
276
+ class Child < Parent
277
+ include Import[:http]
278
+ end
279
+ ----
280
+
281
+ With the above, the child class will have access to both the `logger` and `http` dependencies.
282
+
283
+ ⚠️ Be careful when using parent dependencies within your child classes since they are _private by default_. Even though you can reach them, they might change, which can break your downstream dependencies and probably should be avoided or at least defined as `protected` by your parent objects in order to avoid breaking your parent/child relationship.
284
+
285
+ === Tests
286
+
287
+ As you architect your implementation, you'll want to test your injected dependencies. You'll also want to stub, mock, or spy on them as well. Testing support is built-in for you by only needing to require the stub refinement as provided by this gem. For demonstration purposes, I'm going to assume you are using RSpec but you can adapt for whatever testing framework you are using.
288
+
289
+ Let's say you have the following implementation that combines both {dry-container_link} (or a primitve `Hash` would work too) and this gem:
290
+
291
+ [source,ruby]
292
+ ----
293
+ # Our container with a single dependency.
294
+ module Container
295
+ extend Dry::Container::Mixin
296
+
297
+ register(:kernel) { Kernel }
298
+ end
299
+
300
+ # Our import which defines our container for potential injection.
301
+ Import = Infusible.with Container
302
+
303
+ # Our action class which uses Auto Injector to inject our kernel dependency from our container.
304
+ class Action
305
+ include Import[:kernel]
306
+
307
+ def call = kernel.puts "This is a test."
308
+ end
309
+ ----
310
+
311
+ With our implementation defined, we can test as follows:
312
+
313
+ [source,ruby]
314
+ ----
315
+ # Required: You must require Dry Container and Infusible stubbing for testing purposes.
316
+ require "dry/container/stub"
317
+ require "infusible/stub"
318
+
319
+ RSpec.describe Action do
320
+ # Required: You must refine Infusible to leverage stubbing of your dependencies.
321
+ using Infusible::Stub
322
+
323
+ subject(:action) { Action.new }
324
+
325
+ let(:kernel) { class_spy Kernel }
326
+
327
+ # Required: You must define what dependencies you want to stub and unstub before and after a test.
328
+ before { Import.stub kernel: }
329
+ after { Import.unstub :kernel }
330
+
331
+ describe "#call" do
332
+ it "prints message" do
333
+ action.call
334
+ expect(kernel).to have_received(:puts).with("This is a test.")
335
+ end
336
+ end
337
+ end
338
+ ----
339
+
340
+ Notice that there is very little setup required to test the injected dependencies. You only need to use the refinement and define what you want stubbed in your `before` and `after` blocks. That's it!
341
+
342
+ 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:
343
+
344
+ [source,ruby]
345
+ ----
346
+ # spec/support/shared_contexts/application_container.rb
347
+ require "dry/container/stub"
348
+ require "infusible/stub"
349
+
350
+ RSpec.shared_context "with application dependencies" do
351
+ using Infusible::Stub
352
+
353
+ let(:kernel) { class_spy Kernel }
354
+
355
+ before { Import.stub kernel: }
356
+ after { Import.unstub :kernel }
357
+ end
358
+ ----
359
+
360
+ [source,ruby]
361
+ ----
362
+ # spec/lib/action_spec.rb
363
+ RSpec.describe Action do
364
+ subject(:action) { Action.new }
365
+
366
+ include_context "with application dependencies"
367
+
368
+ describe "#call" do
369
+ it "prints message" do
370
+ action.call
371
+ expect(kernel).to have_received(:puts).with("This is a test.")
372
+ end
373
+ end
374
+ end
375
+ ----
376
+
377
+ A shared context allows you to reuse it across multiple specs by including it as needed.
378
+
379
+ In both spec examples -- so far -- you'll notice only RSpec `before` and `after` blocks are used. You can use an `around` block too. Example:
380
+
381
+ [source,ruby]
382
+ ----
383
+ around do |example|
384
+ Import.stub_with kernel: FakeKernel do
385
+ example.run
386
+ end
387
+ end
388
+ ----
389
+
390
+ ⚠️ I mention `around` block support last because the caveat is that you can't use an `around` block with any RSpec test double since link:https://github.com/rspec/rspec-mocks/issues/1283[RSpec can't guarantee proper cleanup]. This is why the RSpec `before` and `after` blocks were used to guarantee proper setup and teardown. That said, you can use _fakes_ or any object you own which _isn't_ a RSpec test double but provides the Object API you need for testing purposes.
391
+
392
+ == Architecture
393
+
394
+ This gem automates a lot of the boilerplate code you'd normally have to do manually by defining your constructor, initializer, and instance variables for you. Normally, when injecting dependencies, you'd do something like this (using the `Pinger` example provided earlier):
395
+
396
+ [source,ruby]
397
+ ----
398
+ class Pinger
399
+ def initialize http: HTTP, logger: Logger.new(STDOUT)
400
+ @http = http
401
+ @logger = logger
402
+ end
403
+
404
+ def call url
405
+ http.get(url).status.then { |status| logger.info %(The status of "#{url}" is #{status}.) }
406
+ end
407
+
408
+ private
409
+
410
+ attr_reader :http, :logger
411
+ end
412
+ ----
413
+
414
+ When you use this gem all of the construction, initialization, and setting of private instance variables is taken care of for you. So what you see above is identical to the following:
415
+
416
+ [source,ruby]
417
+ ----
418
+ class Pinger
419
+ include Import[:http, :logger]
420
+
421
+ def call url
422
+ http.get(url).status.then { |status| logger.info %(The status of "#{url}" is #{status}.) }
423
+ end
424
+ end
425
+ ----
426
+
427
+ Your constructor, initializer, and instance variables are all there. Only you don't have to write all of this yourself anymore. 🎉
428
+
429
+ == Style Guide
430
+
431
+ When using this gem, along with a container like {dry-container_link}, make sure to adhere to the following guidelines:
432
+
433
+ * Use containers to group related dependencies that make logical sense for the namespace you are working in and avoid using containers as a junk drawer for throwing random objects in.
434
+ * Use containers that don't have a lot of registered dependencies. If you register too many dependencies, then that means your objects are too complex and need to be simplified further.
435
+ * Use the `Import` constant to define _what_ is possible to import much like you'd use a `Container` to define your dependencies. Defining what is importable improves performance and should be defined in separate files for improved fuzzy file finding.
436
+ * Use `**dependencies` as your named double splat argument when defining an initializer which needs to pass injected dependencies upwards. This improves readability and consistency by clearly identifying your injected dependencies.
437
+
438
+ == Development
439
+
440
+ You can also use the IRB console for direct access to all objects:
441
+
442
+ [source,bash]
443
+ ----
444
+ bin/console
445
+ ----
446
+
447
+ == Tests
448
+
449
+ To test, run:
450
+
451
+ [source,bash]
452
+ ----
453
+ bundle exec rake
454
+ ----
455
+
456
+ == link:https://www.alchemists.io/policies/license[License]
457
+
458
+ == link:https://www.alchemists.io/policies/security[Security]
459
+
460
+ == link:https://www.alchemists.io/policies/code_of_conduct[Code of Conduct]
461
+
462
+ == link:https://www.alchemists.io/policies/contributions[Contributions]
463
+
464
+ == link:https://www.alchemists.io/projects/infusible/versions[Versions]
465
+
466
+ == link:https://www.alchemists.io/community[Community]
467
+
468
+ == Credits
469
+
470
+ * Built with link:https://www.alchemists.io/projects/gemsmith[Gemsmith].
471
+ * Engineered by link:https://www.alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].
data/infusible.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "infusible"
5
+ spec.version = "0.0.0"
6
+ spec.authors = ["Brooke Kuhlmann"]
7
+ spec.email = ["brooke@alchemists.io"]
8
+ spec.homepage = "https://www.alchemists.io/projects/infusible"
9
+ spec.summary = "Automates the injection of dependencies into your class."
10
+ spec.license = "Hippocratic-2.1"
11
+
12
+ spec.metadata = {
13
+ "bug_tracker_uri" => "https://github.com/bkuhlmann/infusible/issues",
14
+ "changelog_uri" => "https://www.alchemists.io/projects/infusible/versions",
15
+ "documentation_uri" => "https://www.alchemists.io/projects/infusible",
16
+ "funding_uri" => "https://github.com/sponsors/bkuhlmann",
17
+ "label" => "Infusible",
18
+ "rubygems_mfa_required" => "true",
19
+ "source_code_uri" => "https://github.com/bkuhlmann/infusible"
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.1"
26
+ spec.add_dependency "marameters", "~> 0.8"
27
+ spec.add_dependency "zeitwerk", "~> 2.6"
28
+
29
+ spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
30
+ spec.files = Dir["*.gemspec", "lib/**/*"]
31
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Infusible
4
+ # Associates the container with the constructor for actualization.
5
+ class Actuator
6
+ def initialize container, constructor: Infusible::Constructor
7
+ @container = container
8
+ @constructor = constructor
9
+ end
10
+
11
+ def [](*configuration) = constructor.new container, *configuration
12
+
13
+ private
14
+
15
+ attr_reader :container, :constructor
16
+ end
17
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "marameters"
4
+
5
+ module Infusible
6
+ # Provides the automatic and complete resolution of all injected dependencies.
7
+ class Constructor < Module
8
+ def initialize container, *configuration
9
+ super()
10
+
11
+ @container = container
12
+ @dependencies = DependencyMap.new(*configuration)
13
+ @class_module = Class.new(Module).new
14
+ @instance_module = Class.new(Module).new
15
+ end
16
+
17
+ def included klass
18
+ super
19
+ define klass
20
+ klass.extend class_module
21
+ klass.include instance_module
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :container, :dependencies, :class_module, :instance_module
27
+
28
+ def define klass
29
+ define_new
30
+ define_initialize klass
31
+ define_readers
32
+ end
33
+
34
+ def define_new
35
+ class_module.class_exec container, dependencies.to_h do |container, collection|
36
+ define_method :new do |*positionals, **keywords, &block|
37
+ collection.each { |name, id| keywords[name] = container[id] unless keywords.key? name }
38
+ super(*positionals, **keywords, &block)
39
+ end
40
+ end
41
+ end
42
+
43
+ def define_initialize klass
44
+ super_parameters = Marameters.of(klass, :initialize).map do |instance|
45
+ break instance unless instance.only_bare_splats?
46
+ end
47
+
48
+ if super_parameters.positionals? || super_parameters.only_single_splats?
49
+ define_initialize_with_positionals super_parameters
50
+ else
51
+ define_initialize_with_keywords super_parameters
52
+ end
53
+ end
54
+
55
+ def define_initialize_with_positionals super_parameters
56
+ instance_module.class_exec dependencies.names, method(:define_variables) do |names, definer|
57
+ define_method :initialize do |*positionals, **keywords, &block|
58
+ definer.call self, keywords
59
+
60
+ if super_parameters.only_single_splats?
61
+ super(*positionals, **keywords, &block)
62
+ else
63
+ super(*positionals, **super_parameters.keyword_slice(keywords, keys: names), &block)
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def define_initialize_with_keywords super_parameters
70
+ instance_module.class_exec dependencies.names, method(:define_variables) do |names, definer|
71
+ define_method :initialize do |**keywords, &block|
72
+ definer.call self, keywords
73
+ super(**super_parameters.keyword_slice(keywords, keys: names), &block)
74
+ end
75
+ end
76
+ end
77
+
78
+ # :reek:FeatureEnvy
79
+ def define_variables target, keywords
80
+ dependencies.names.each do |name|
81
+ next unless keywords.key?(name) || !target.instance_variable_defined?(:"@#{name}")
82
+
83
+ target.instance_variable_set :"@#{name}", keywords[name]
84
+ end
85
+ end
86
+
87
+ def define_readers
88
+ methods = dependencies.names.map { |name| ":#{name}" }
89
+
90
+ instance_module.class_eval <<-READERS, __FILE__, __LINE__ + 1
91
+ private attr_reader #{methods.join ", "}
92
+ READERS
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Infusible
4
+ # Sanitizes and resolves dependencies for use.
5
+ class DependencyMap
6
+ NAME_PATTERN = /([a-z_][a-zA-Z_0-9]*)$/
7
+
8
+ attr_reader :names
9
+
10
+ def initialize *configuration, name_pattern: NAME_PATTERN
11
+ @name_pattern = name_pattern
12
+ @collection = {}
13
+
14
+ configuration = configuration.dup
15
+ aliases = configuration.last.is_a?(Hash) ? configuration.pop : {}
16
+
17
+ configuration.each { |identifier| add to_name(identifier), identifier }
18
+ aliases.each { |name, identifier| add name, identifier }
19
+
20
+ @names = collection.keys
21
+ end
22
+
23
+ def to_h = collection
24
+
25
+ private
26
+
27
+ attr_reader :name_pattern, :collection
28
+
29
+ def to_name identifier
30
+ identifier.to_s[name_pattern] || fail(Errors::InvalidDependency.new(identifier:))
31
+ end
32
+
33
+ def add name, identifier
34
+ name = name.to_sym
35
+
36
+ return collection[name] = identifier unless collection.key? name
37
+
38
+ fail Errors::DuplicateDependency.new name:, identifier:
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Infusible
4
+ module Errors
5
+ # Prevents duplicate dependencies from being injected.
6
+ class DuplicateDependency < StandardError
7
+ def initialize name:, identifier:
8
+ super "Remove #{identifier.inspect} since it's a duplicate of #{name.inspect}."
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Infusible
4
+ module Errors
5
+ # Prevents improperly named dependencies from being injected.
6
+ class InvalidDependency < StandardError
7
+ def initialize identifier:
8
+ super "Cannot use #{identifier.inspect} as an identifier."
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "infusible"
4
+
5
+ module Infusible
6
+ # Provides stubbing of the injected container when used in a test framework.
7
+ module Stub
8
+ refine Actuator do
9
+ def stub_with(pairs, &)
10
+ return unless block_given?
11
+
12
+ container.is_a?(Hash) ? stub_hash_with(pairs, &) : stub_container_with(pairs, &)
13
+ end
14
+
15
+ def stub(pairs) = container.is_a?(Hash) ? stub_hash(pairs) : stub_container(pairs)
16
+
17
+ def unstub(*keys) = container.is_a?(Hash) ? unstub_hash(*keys) : unstub_container(*keys)
18
+
19
+ private
20
+
21
+ def stub_container_with pairs
22
+ stub_container pairs
23
+ yield
24
+ unstub_container(*pairs.keys)
25
+ end
26
+
27
+ def stub_container pairs
28
+ container.enable_stubs!
29
+ pairs.each { |key, value| container.stub key, value }
30
+ end
31
+
32
+ def unstub_container(*keys) = keys.each { |key| container.unstub key }
33
+
34
+ def stub_hash_with pairs
35
+ stub_hash pairs
36
+ yield
37
+ unstub_hash(*pairs.keys)
38
+ end
39
+
40
+ def stub_hash pairs
41
+ @backup = container.dup
42
+ container.merge! pairs
43
+ end
44
+
45
+ def unstub_hash(*keys) = container.merge! @backup.slice(*keys)
46
+ end
47
+ end
48
+ end
data/lib/infusible.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ Zeitwerk::Loader.for_gem.then do |loader|
6
+ loader.ignore "#{__dir__}/infusible/stub"
7
+ loader.setup
8
+ end
9
+
10
+ # Main namespace.
11
+ module Infusible
12
+ def self.with(container) = Actuator.new container
13
+ end
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: infusible
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
+ MIIC/jCCAeagAwIBAgIBBTANBgkqhkiG9w0BAQsFADAlMSMwIQYDVQQDDBpicm9v
14
+ a2UvREM9YWxjaGVtaXN0cy9EQz1pbzAeFw0yMjAzMTkxNzI0MzJaFw0yMzAzMTkx
15
+ NzI0MzJaMCUxIzAhBgNVBAMMGmJyb29rZS9EQz1hbGNoZW1pc3RzL0RDPWlvMIIB
16
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6l1qpXTiomH1RfMRloyw7MiE
17
+ xyVx/x8Yc3EupdH7uhNaTXQGyORN6aOY//1QXXMHIZ9tW74nZLhesWMSUMYy0XhB
18
+ brs+KkurHnc9FnEJAbG7ebGvl/ncqZt72nQvaxpDxvuCBHgJAz+8i5wl6FhLw+oT
19
+ 9z0A8KcGhz67SdcoQiD7qiCjL/2NTeWHOzkpPrdGlt088+VerEEGf5I13QCvaftP
20
+ D5vkU0YlAm1r98BymuJlcQ1qdkVEI1d48ph4kcS0S0nv1RiuyVb6TCAR3Nu3VaVq
21
+ 3fPzZKJLZBx67UvXdbdicWPiUR75elI4PXpLIic3xytaF52ZJYyKZCNZJhNwfQID
22
+ AQABozkwNzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU0nzow9vc
23
+ 2CdikiiE3fJhP/gY4ggwDQYJKoZIhvcNAQELBQADggEBAJbbNyWzFjqUNVPPCUCo
24
+ IMrhDa9xf1xkORXNYYbmXgoxRy/KyNbUr+jgEEoWJAm9GXlcqxxWAUI6pK/i4/Qi
25
+ X6rPFEFmeObDOHNvuqy8Hd6AYsu+kP94U/KJhe9wnWGMmGoNKJNU3EkW3jM/osSl
26
+ +JRxiH5t4WtnDiVyoYl5nYC02rYdjJkG6VMxDymXTqn7u6HhYgZkGujq1UPar8x2
27
+ hNIWJblDKKSu7hA2d6+kUthuYo13o1sg1Da/AEDg0hoZSUvhqDEF5Hy232qb3pDt
28
+ CxDe2+VuChj4I1nvIHdu+E6XoEVlanUPKmSg6nddhkKn2gC45Kyzh6FZqnzH/CRp
29
+ RFE=
30
+ -----END CERTIFICATE-----
31
+ date: 2022-09-05 00:00:00.000000000 Z
32
+ dependencies:
33
+ - !ruby/object:Gem::Dependency
34
+ name: marameters
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.8'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.8'
47
+ - !ruby/object:Gem::Dependency
48
+ name: zeitwerk
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.6'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.6'
61
+ description:
62
+ email:
63
+ - brooke@alchemists.io
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files:
67
+ - README.adoc
68
+ - LICENSE.adoc
69
+ files:
70
+ - LICENSE.adoc
71
+ - README.adoc
72
+ - infusible.gemspec
73
+ - lib/infusible.rb
74
+ - lib/infusible/actuator.rb
75
+ - lib/infusible/constructor.rb
76
+ - lib/infusible/dependency_map.rb
77
+ - lib/infusible/errors/duplicate_dependency.rb
78
+ - lib/infusible/errors/invalid_dependency.rb
79
+ - lib/infusible/stub.rb
80
+ homepage: https://www.alchemists.io/projects/infusible
81
+ licenses:
82
+ - Hippocratic-2.1
83
+ metadata:
84
+ bug_tracker_uri: https://github.com/bkuhlmann/infusible/issues
85
+ changelog_uri: https://www.alchemists.io/projects/infusible/versions
86
+ documentation_uri: https://www.alchemists.io/projects/infusible
87
+ funding_uri: https://github.com/sponsors/bkuhlmann
88
+ label: Infusible
89
+ rubygems_mfa_required: 'true'
90
+ source_code_uri: https://github.com/bkuhlmann/infusible
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '3.1'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.3.21
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Automates the injection of dependencies into your class.
110
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1,3 @@
1
+ +ۏ[Gpз}�߮!�:;��� F�E��c8�&ٍ;DH
2
+ `��??����J 1�6 �7��D��d ������^ΰR�jtk���O P����1b!��Z��8�Q����]Ɏ~��c�4�e��2��v���f�iGw���)��2^J�oߋy�<�+B6�fw�ԮL�Q���</�k�ε%mM�sŐ2 �f�`ЌO=@S��4�<�
3
+ Rd#O�uz�O��)���=W1������>��uRk�C��k�,�KFuu���