maglev-injectable 2.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ruby.yml +25 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +30 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +360 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/injectable/class_methods.rb +159 -0
- data/lib/injectable/dependencies_graph.rb +40 -0
- data/lib/injectable/dependency.rb +76 -0
- data/lib/injectable/instance_methods.rb +68 -0
- data/lib/injectable/method_already_exists_exception.rb +4 -0
- data/lib/injectable/missing_dependencies_exception.rb +4 -0
- data/lib/injectable/version.rb +3 -0
- data/lib/injectable.rb +62 -0
- data/maglev-injectable.gemspec +29 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 032be6c3740e077577ece58660af3636a98adb9cc13272ab12e42f3366724dad
|
4
|
+
data.tar.gz: bb6549d739e3e2a5a49fd134436c3874fa4f78e9825d8b795ce9a182cdf28207
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ff4c8dd10329368fe28ad93d552ced0a109ab34234a7e7c375989423da9acfdbef467c3e1bf9bf8af6d93d37aa8446c7ad5c5a8ffc43db162923608378b49e6e
|
7
|
+
data.tar.gz: '0856a305f51edb5df46c84201c19108220c2af77943cd5bd679e6127c0a82f7004ba128f727eb08b307e03e0ce6523dede3aeb1208c6d7ace7e44c021e04abc2'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
build:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
strategy:
|
13
|
+
matrix:
|
14
|
+
ruby: [ 2.5.x, 2.6, 2.7.x ]
|
15
|
+
steps:
|
16
|
+
- uses: actions/checkout@v2
|
17
|
+
- name: Set up Ruby ${{ matrix.ruby }}
|
18
|
+
uses: actions/setup-ruby@v1
|
19
|
+
with:
|
20
|
+
ruby-version: ${{ matrix.ruby }}
|
21
|
+
- name: Build and test with Rake
|
22
|
+
run: |
|
23
|
+
gem install bundler
|
24
|
+
bundle install --jobs 4 --retry 3
|
25
|
+
bundle exec rake
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
### Unreleased
|
2
|
+
|
3
|
+
### 2.1.1 - 2021-03-15
|
4
|
+
|
5
|
+
* bug fixes
|
6
|
+
* Address final Ruby 2.7 warnings (#24)
|
7
|
+
|
8
|
+
### 2.1.0 - 2021-01-05
|
9
|
+
|
10
|
+
* enhancements
|
11
|
+
* Return `method` object instead of monkey patched instance (#17)
|
12
|
+
* Prepare for Ruby 3.0 (#22)
|
13
|
+
|
14
|
+
### 2.0.0 - 2020-03-16
|
15
|
+
|
16
|
+
* breaking changes
|
17
|
+
* Raises exception if shadowing an existing `#call` method in a dependency (#15)
|
18
|
+
|
19
|
+
### 1.0.3 - 2020-03-16
|
20
|
+
|
21
|
+
* enhancements
|
22
|
+
* Added GitHub Actions as CI (#9)
|
23
|
+
|
24
|
+
* bug fixes
|
25
|
+
* Fixed a bug that wouldn't pass a block if `#call` was aliased (#7)
|
26
|
+
|
27
|
+
### 1.0.2 - 2020-03-02
|
28
|
+
|
29
|
+
* security
|
30
|
+
* Bump `rake` to 12.3.3 (#11)
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at dev@rubiconmd.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
maglev-injectable (2.1.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
byebug (11.1.3)
|
10
|
+
coderay (1.1.3)
|
11
|
+
diff-lcs (1.4.4)
|
12
|
+
method_source (1.0.0)
|
13
|
+
pry (0.13.1)
|
14
|
+
coderay (~> 1.1)
|
15
|
+
method_source (~> 1.0)
|
16
|
+
pry-byebug (3.9.0)
|
17
|
+
byebug (~> 11.0)
|
18
|
+
pry (~> 0.13.0)
|
19
|
+
rake (13.0.3)
|
20
|
+
rspec (3.10.0)
|
21
|
+
rspec-core (~> 3.10.0)
|
22
|
+
rspec-expectations (~> 3.10.0)
|
23
|
+
rspec-mocks (~> 3.10.0)
|
24
|
+
rspec-core (3.10.1)
|
25
|
+
rspec-support (~> 3.10.0)
|
26
|
+
rspec-expectations (3.10.1)
|
27
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
+
rspec-support (~> 3.10.0)
|
29
|
+
rspec-mocks (3.10.1)
|
30
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
31
|
+
rspec-support (~> 3.10.0)
|
32
|
+
rspec-support (3.10.1)
|
33
|
+
|
34
|
+
PLATFORMS
|
35
|
+
ruby
|
36
|
+
|
37
|
+
DEPENDENCIES
|
38
|
+
bundler (~> 2.0)
|
39
|
+
maglev-injectable!
|
40
|
+
pry-byebug
|
41
|
+
rake (~> 13.0)
|
42
|
+
rspec (~> 3.0)
|
43
|
+
|
44
|
+
BUNDLED WITH
|
45
|
+
2.2.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 amrocco
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,360 @@
|
|
1
|
+
# Injectable
|
2
|
+
|
3
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/a45cc5935a5c16b837ed/maintainability)](https://codeclimate.com/github/rubiconmd/injectable/maintainability)![Ruby](https://github.com/rubiconmd/injectable/workflows/Ruby/badge.svg)
|
4
|
+
|
5
|
+
`Injectable` is an opinionated and declarative [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) library for ruby.
|
6
|
+
|
7
|
+
It is being used in production (under ruby 2.5.1) in [RubiconMD](https://github.com/rubiconmd) and was extracted from its codebase.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'injectable', '>= 1.0.0'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install injectable
|
24
|
+
|
25
|
+
## Motivation
|
26
|
+
|
27
|
+
The main motivation of `Injectable` is to ease compliance with [SOLID's](https://en.wikipedia.org/wiki/SOLID)\*, [SRP](https://en.wikipedia.org/wiki/Single_responsibility_principle)\* and [Dependency Inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) by providing a declarative and very readable [DSL](https://en.wikipedia.org/wiki/Domain-specific_language)\* which avoids lots of bolierplate code and thus encourages good practices.*
|
28
|
+
|
29
|
+
*Sorry about the acronyms, but using an [Ubiquitous Language](https://martinfowler.com/bliki/UbiquitousLanguage.html) is important.
|
30
|
+
|
31
|
+
### Encapsulate domain logic
|
32
|
+
|
33
|
+
Using Ruby on Rails recommended practices as an example, when your application grows enough you usually end up with huge model classes with too many responsibilities.
|
34
|
+
|
35
|
+
It's way better (although it requires effort and discipline) to split those models and extract domain logic into [Service Objects](https://martinfowler.com/bliki/AnemicDomainModel.html) ("SOs" from now on). You can do this without `Injectable`, but `Injectable` will make your SOs way more readable and a pleasure not only to write but also to test, while encouraging general good practices.
|
36
|
+
|
37
|
+
### Avoiding to hardcode dependencies
|
38
|
+
|
39
|
+
If you find occurences of `SomeClass.any_instance.expects(:method)` in your **unit** tests, then you are probably hardcoding dependencies:
|
40
|
+
|
41
|
+
```rb
|
42
|
+
test "MyClass#call"
|
43
|
+
Collaborator.any_instance.expects(:submit!) # hardcoded dependency
|
44
|
+
MyClass.new.call
|
45
|
+
end
|
46
|
+
|
47
|
+
class MyClass
|
48
|
+
attr_reader :collaborator
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
@collaborator = Collaborator.new
|
52
|
+
end
|
53
|
+
|
54
|
+
def call
|
55
|
+
collaborator.submit!
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
What if you did this instead:
|
61
|
+
|
62
|
+
```rb
|
63
|
+
test "MyClass#call"
|
64
|
+
collaborator = stub('Collaborator')
|
65
|
+
collaborator.expects(:submit!)
|
66
|
+
MyClass.new(collaborator: collaborator).call
|
67
|
+
end
|
68
|
+
|
69
|
+
class MyClass
|
70
|
+
attr_reader :collaborator
|
71
|
+
|
72
|
+
def initialize(collaborator: Collaborator.new) # we will just provide a default
|
73
|
+
@collaborator = collaborator
|
74
|
+
end
|
75
|
+
|
76
|
+
def call
|
77
|
+
collaborator.submit!
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
The benefits are not only for testing, as now your class is more modular and you can swap collaborators as long as they have the proper interface, in this case they have to `respond_to :submit!`
|
83
|
+
|
84
|
+
`Injectable` allows you to write the above code like this:
|
85
|
+
|
86
|
+
```rb
|
87
|
+
class MyClass
|
88
|
+
include Injectable
|
89
|
+
|
90
|
+
dependency :collaborator
|
91
|
+
|
92
|
+
def call
|
93
|
+
collaborator.submit!
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
It might not seem a lot but:
|
99
|
+
|
100
|
+
1. Imagine that you have 4 dependencies. That's a lot of boilerplate.
|
101
|
+
2. `Injectable` is not only this, it has many more features. Please keep reading.
|
102
|
+
|
103
|
+
## Usage example
|
104
|
+
|
105
|
+
`Injectable` is a mixin that you have to include in your class and it will provide several macros.
|
106
|
+
|
107
|
+
This is a real world example:
|
108
|
+
|
109
|
+
```rb
|
110
|
+
class PdfGenerator
|
111
|
+
include Injectable
|
112
|
+
|
113
|
+
dependency :wicked_pdf
|
114
|
+
|
115
|
+
argument :html
|
116
|
+
argument :render_footer, default: false
|
117
|
+
|
118
|
+
def call
|
119
|
+
wicked_pdf.pdf_from_string(html, options)
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def options
|
125
|
+
return {} unless render_footer
|
126
|
+
|
127
|
+
{
|
128
|
+
footer: {
|
129
|
+
left: footer,
|
130
|
+
}
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
def footer
|
135
|
+
"Copyright ® #{Time.current.year}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# And you would use it like this:
|
140
|
+
PdfGenerator.call(html: '<some html here>')
|
141
|
+
# Overriding the wicked_pdf dependency:
|
142
|
+
PdfGenerator.new(wicked_pdf: wicked_pdf_replacement).call(html: '<some html>')
|
143
|
+
```
|
144
|
+
|
145
|
+
## Premises
|
146
|
+
|
147
|
+
In order to understand how (and why) `Injectable` works, you need to know some principles.
|
148
|
+
|
149
|
+
### #1 The `#call` method
|
150
|
+
|
151
|
+
`Injectable` classes **must define a public `#call` method that takes no arguments**.
|
152
|
+
|
153
|
+
This is **the only public method** you will be defining in your `Injectable` classes.
|
154
|
+
|
155
|
+
```rb
|
156
|
+
# Correct ✅
|
157
|
+
def call
|
158
|
+
# do stuff
|
159
|
+
end
|
160
|
+
|
161
|
+
# Wrong ❗️
|
162
|
+
def call(some_argument)
|
163
|
+
# won't work and will raise an exception at runtime
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
If you want your `#call` method to receive arguments, that's what the `#argument` macro is for. BTW, we call those **runtime arguments**.
|
168
|
+
|
169
|
+
Why `#call`?
|
170
|
+
|
171
|
+
Because it's a ruby idiom. Many things in ruby are `callable`, like lambdas.
|
172
|
+
|
173
|
+
### #2 The `initialize` method
|
174
|
+
|
175
|
+
Injectable classes take their **dependencies as keyword arguments** on the `initialize` method. They can also take **configuration arguments** on `initialize`:
|
176
|
+
|
177
|
+
```rb
|
178
|
+
MyClass.new(some_dep: some_dep_instance, some_config: true).call
|
179
|
+
```
|
180
|
+
|
181
|
+
`Injectable` instantiates **dependencies that you have declared with the `dependency` macro** for you and passes them to `initialize`, so if you don't want to override those you don't even need to instantiate the class and you can use the provided class method **`#call` shortcut**:
|
182
|
+
|
183
|
+
```rb
|
184
|
+
Myclass.call # This is calling `initialize` under the hood
|
185
|
+
```
|
186
|
+
|
187
|
+
If you need to override dependencies or configuration options, just call `new` yourself:
|
188
|
+
|
189
|
+
```rb
|
190
|
+
Myclass.new(some_dep: Override.new, some_config: false).call
|
191
|
+
```
|
192
|
+
|
193
|
+
If you do that, **any dependency that you didn't pass will be injected by `Injectable`**.
|
194
|
+
Notice that **configuration arguments**, which are declared with `#initialize_with` behave in the exact same way.
|
195
|
+
|
196
|
+
### #3 Keyword arguments
|
197
|
+
|
198
|
+
Both `#initialize` and `#call` take **keyword arguments**.
|
199
|
+
|
200
|
+
### #4 Readers
|
201
|
+
|
202
|
+
All `Injectable` macros define reader methods for you, that's why you define `#call` without arguments, because **you access everything you declare via reader methods**.
|
203
|
+
|
204
|
+
## The `#dependency` macro
|
205
|
+
|
206
|
+
This is the main reason why you want to use this library in the first place.
|
207
|
+
|
208
|
+
There are several ways of declaring a `#dependency`:
|
209
|
+
|
210
|
+
### Bare dependency name
|
211
|
+
|
212
|
+
```rb
|
213
|
+
class ReportPdfRenderer
|
214
|
+
include Injectable
|
215
|
+
|
216
|
+
dependency :some_dependency
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
1. `Injectable` first tries to find the `SomeDependency` constant in `ReportPdfRenderer`namespace.
|
221
|
+
2. If it doesn't find it, then tries without namespace (`::SomeDependency`).
|
222
|
+
|
223
|
+
Notice that this happens **at runtime**, not when defining your class.
|
224
|
+
|
225
|
+
### Explicit, inline class:
|
226
|
+
|
227
|
+
```rb
|
228
|
+
class MyInjectable
|
229
|
+
include Injectable
|
230
|
+
|
231
|
+
dependency :client, class: Aws::S3::Client
|
232
|
+
dependency :parser, class: VeryLongClassNameForMyParser
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
236
|
+
Nothing fancy here, you are explicitly telling `Injectable` which class to instantiate for you.
|
237
|
+
|
238
|
+
You will want to use this style for example if the class is namespaced somewhere else or if you want a different name other than the class', like for example if it's too long.
|
239
|
+
|
240
|
+
Notice that this approach sets the class when ruby interprets the class, **not at runtime**.
|
241
|
+
|
242
|
+
### With a block:
|
243
|
+
|
244
|
+
```rb
|
245
|
+
dependency :complex_client do
|
246
|
+
instance = ThirdPartyLib.new(:foo, bar: 'goo')
|
247
|
+
instance.set_config(:name, 'value')
|
248
|
+
instance
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
It's important to understand that `Injectable` won't call `#new` on whatever you return from this block.
|
253
|
+
|
254
|
+
You probably want to use this when your dependency has a complex setup. We use it a lot when wrapping third party libraries which aren't reused elsewhere.
|
255
|
+
|
256
|
+
If you want to wrap a third party library and you need to reuse it, then we recommend that you write a specific `Injectable` class for it, so it adheres to its principles and is easier to use.
|
257
|
+
|
258
|
+
### `#dependency` options
|
259
|
+
|
260
|
+
#### `:with`
|
261
|
+
|
262
|
+
If the dependency takes arguments, you can set them with :with
|
263
|
+
|
264
|
+
```rb
|
265
|
+
# Arrays will be splatted: WithNormalArguments.new(1, 2, 3)
|
266
|
+
dependency :with_normal_arguments, with: [1, 2, 3]
|
267
|
+
# Hashes will be passed as-is: WithKeywordArguments.new(foo: 'bar)
|
268
|
+
dependency :with_keyword_arguments, with: { foo: 'bar' }
|
269
|
+
```
|
270
|
+
|
271
|
+
### `:depends_on`
|
272
|
+
|
273
|
+
It allows you to share **memoized instances** of dependencies and supports both a single dependency or multiples as an Array:
|
274
|
+
|
275
|
+
```rb
|
276
|
+
dependency :client # this will be instantiated just once and will be shared
|
277
|
+
dependency :reporter, depends_on: :client
|
278
|
+
dependency :mailer, depends_on: %i[client reporter]
|
279
|
+
```
|
280
|
+
|
281
|
+
Dependencies of dependencies will be passed as keyword arguments using the same name they were declared with. In the example above, `Injectable` will instantiate a `Mailer` class passing `{ client: client, reporter: reporter }` to `#initialize`.
|
282
|
+
|
283
|
+
If you have a dependency that is defined with a block which also depends_on other dependencies, you'll receive those as keyword arguments:
|
284
|
+
|
285
|
+
```rb
|
286
|
+
dependency :my_dependency, depends_on: :client do |client:|
|
287
|
+
MyDependency.new(client)
|
288
|
+
end
|
289
|
+
```
|
290
|
+
|
291
|
+
### `:call`
|
292
|
+
|
293
|
+
Sometimes you have a class that doesn't adhere to `Injectable` principles:
|
294
|
+
|
295
|
+
```rb
|
296
|
+
dependency :renderer
|
297
|
+
|
298
|
+
def call
|
299
|
+
renderer.render # this class does not respond to `call`
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
`:call` is a way of wrapping such dependency so it behaves like an `Injectable`:
|
304
|
+
|
305
|
+
```rb
|
306
|
+
dependency :renderer, call: :render
|
307
|
+
|
308
|
+
def call
|
309
|
+
renderer.call
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
It's important to understand that **you can mix and match all dependency configurations and options** described above.
|
314
|
+
|
315
|
+
## `#initialize_with` macro
|
316
|
+
|
317
|
+
This macro is meant for **configuration arguments** passed to `initialize`:
|
318
|
+
|
319
|
+
```rb
|
320
|
+
initialize_with :debug, default: false
|
321
|
+
```
|
322
|
+
|
323
|
+
If you don't pass the `:default` option the argument will be required.
|
324
|
+
|
325
|
+
## `#argument` macro
|
326
|
+
|
327
|
+
`#argument` allows you to define **runtime arguments** passed to `#call`
|
328
|
+
|
329
|
+
```rb
|
330
|
+
argument :browser, default: 'Unknown'
|
331
|
+
```
|
332
|
+
|
333
|
+
If you don't pass the `:default` option the argument will be required.
|
334
|
+
|
335
|
+
|
336
|
+
## Development
|
337
|
+
|
338
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
339
|
+
|
340
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
341
|
+
|
342
|
+
## Contributing
|
343
|
+
|
344
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rubiconmd/injectable. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
345
|
+
|
346
|
+
## License
|
347
|
+
|
348
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
349
|
+
|
350
|
+
## Code of Conduct
|
351
|
+
|
352
|
+
Everyone interacting in the Injectable project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
353
|
+
|
354
|
+
## Credits
|
355
|
+
|
356
|
+
- [RubiconMD](https://github.com/rubiconmd) allowed extracting this gem from its codebase and release it as open source.
|
357
|
+
- [Durran Jordan](https://github.com/durran) allowed the usage of the gem name at rubygems.org.
|
358
|
+
- [David Marchante](https://github.com/iovis) brainstormed the `initialize`/`call` approach, did all code reviews and provided lots of insightful feedback and suggestions. He also wrote the inline documentation.
|
359
|
+
- [Julio Antequera](https://github.com/jantequera), [Jimmi Carney](https://github.com/ayoformayo) and [Anthony Rocco](https://github.com/amrocco) had the patience to use it and report many bugs. Also most of the features in this gem came up when reviewing their usage of it. Anthony also made the effort of extracting the code from RubiconMD's codebase.
|
360
|
+
- [Rodrigo Álvarez](https://github.com/Papipo) had the idea for the DSL and actually wrote the library.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "injectable"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
module Injectable
|
2
|
+
module ClassMethods
|
3
|
+
def self.extended(base)
|
4
|
+
base.class_eval do
|
5
|
+
simple_class_attribute :dependencies,
|
6
|
+
:call_arguments,
|
7
|
+
:initialize_arguments
|
8
|
+
|
9
|
+
self.dependencies = DependenciesGraph.new(namespace: base)
|
10
|
+
self.initialize_arguments = {}
|
11
|
+
self.call_arguments = {}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def inherited(base)
|
16
|
+
base.class_eval do
|
17
|
+
self.dependencies = dependencies.with_namespace(base)
|
18
|
+
self.initialize_arguments = initialize_arguments.dup
|
19
|
+
self.call_arguments = call_arguments.dup
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Blatantly stolen from rails' ActiveSupport.
|
24
|
+
# This is a simplified version of class_attribute
|
25
|
+
def simple_class_attribute(*attrs)
|
26
|
+
attrs.each do |name|
|
27
|
+
define_singleton_method(name) { nil }
|
28
|
+
|
29
|
+
ivar = "@#{name}"
|
30
|
+
|
31
|
+
define_singleton_method("#{name}=") do |val|
|
32
|
+
singleton_class.class_eval do
|
33
|
+
define_method(name) { val }
|
34
|
+
end
|
35
|
+
|
36
|
+
if singleton_class?
|
37
|
+
class_eval do
|
38
|
+
define_method(name) do
|
39
|
+
if instance_variable_defined? ivar
|
40
|
+
instance_variable_get ivar
|
41
|
+
else
|
42
|
+
singleton_class.send name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
val
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Use the service with the params declared with '.argument'
|
53
|
+
# @param args [Hash] parameters needed for the Service
|
54
|
+
# @example MyService.call(foo: 'first_argument', bar: 'second_argument')
|
55
|
+
def call(args = {})
|
56
|
+
new.call(args)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Declare dependencies for the service
|
60
|
+
# @param name [Symbol] the name of the service
|
61
|
+
# @option options [Class] :class The class to use if it's different from +name+
|
62
|
+
# @option options [Symbol, Array<Symbol>] :depends_on if the dependency has more dependencies
|
63
|
+
# @yield explicitly declare the dependency
|
64
|
+
#
|
65
|
+
# @return [Object] the injected dependency
|
66
|
+
#
|
67
|
+
# @example Using the same name as the service object
|
68
|
+
# dependency :team_query
|
69
|
+
# # => @team_query = TeamQuery.new
|
70
|
+
#
|
71
|
+
# @example Specifying a different class
|
72
|
+
# dependency :player_query, class: UserQuery
|
73
|
+
# # => @player_query = UserQuery.new
|
74
|
+
#
|
75
|
+
# @example With a block
|
76
|
+
# dependency :active_players do
|
77
|
+
# ->(players) { players.select(&:active?) }
|
78
|
+
# end
|
79
|
+
# # => @active_players = [lambda]
|
80
|
+
#
|
81
|
+
# @example With more dependencies
|
82
|
+
# dependency :counter
|
83
|
+
# dependency :team_service
|
84
|
+
# dependency :player_counter, depends_on: [:counter, :team_service]
|
85
|
+
# # => @counter = Counter.new
|
86
|
+
# # => @team_service = TeamService.new
|
87
|
+
# # => @player_counter = PlayerCounter.new(counter: @counter, team_service: @team_service)
|
88
|
+
#
|
89
|
+
# @example Dependencies that don't accept keyword arguments
|
90
|
+
# dependency :counter
|
91
|
+
# dependency :player_counter, depends_on: :counter do |counter:|
|
92
|
+
# PlayerCounter.new(counter)
|
93
|
+
# end
|
94
|
+
# # => @counter = Counter.new
|
95
|
+
# # => @player_counter = PlayerCounter.new(@counter)
|
96
|
+
def dependency(name, options = {}, &block)
|
97
|
+
options[:block] = block if block_given?
|
98
|
+
options[:depends_on] = Array(options.fetch(:depends_on, []))
|
99
|
+
options[:name] = name
|
100
|
+
dependencies.add(**options)
|
101
|
+
define_method name do
|
102
|
+
return instance_variable_get("@#{name}") if instance_variable_defined?("@#{name}")
|
103
|
+
|
104
|
+
instance_variable_set "@#{name}", instantiate_dependency(name)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Declare the arguments for `#call` and initialize the accessors
|
109
|
+
# This helps us clean up the code for memoization:
|
110
|
+
#
|
111
|
+
# ```
|
112
|
+
# private
|
113
|
+
#
|
114
|
+
# def player
|
115
|
+
# # player_id exists in the context because we added it as an argument
|
116
|
+
# @player ||= player_query.call(player_id)
|
117
|
+
# end
|
118
|
+
# ```
|
119
|
+
#
|
120
|
+
# Every argument is required unless given an optional default value
|
121
|
+
# @param name Name of the argument
|
122
|
+
# @option options :default The default value of the argument
|
123
|
+
# @example
|
124
|
+
# argument :player_id
|
125
|
+
# # => def call(player_id:)
|
126
|
+
# # => @player_id = player_id
|
127
|
+
# # => end
|
128
|
+
# @example with default arguments
|
129
|
+
# argument :team_id, default: 1
|
130
|
+
# # => def call(team_id: 1)
|
131
|
+
# # => @team_id = team_id
|
132
|
+
# # => end
|
133
|
+
def argument(name, options = {})
|
134
|
+
call_arguments[name] = options
|
135
|
+
attr_accessor name
|
136
|
+
end
|
137
|
+
|
138
|
+
def initialize_with(name, options = {})
|
139
|
+
initialize_arguments[name] = options
|
140
|
+
attr_accessor name
|
141
|
+
end
|
142
|
+
|
143
|
+
# Get the #initialize arguments declared with '.initialize_with' with no default
|
144
|
+
# @private
|
145
|
+
def required_initialize_arguments
|
146
|
+
find_required_arguments initialize_arguments
|
147
|
+
end
|
148
|
+
|
149
|
+
# Get the #call arguments declared with '.argument' with no default
|
150
|
+
# @private
|
151
|
+
def required_call_arguments
|
152
|
+
find_required_arguments call_arguments
|
153
|
+
end
|
154
|
+
|
155
|
+
def find_required_arguments(hash)
|
156
|
+
hash.reject { |_arg, options| options.key?(:default) }.keys
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Injectable
|
4
|
+
# Holds the dependency signatures of the service object
|
5
|
+
class DependenciesGraph
|
6
|
+
attr_reader :graph, :dependency_class
|
7
|
+
attr_accessor :namespace
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :@graph, :[]
|
10
|
+
|
11
|
+
def initialize(namespace:, dependency_class: ::Injectable::Dependency)
|
12
|
+
@namespace = namespace
|
13
|
+
@graph = {}
|
14
|
+
@dependency_class = dependency_class
|
15
|
+
end
|
16
|
+
|
17
|
+
def names
|
18
|
+
graph.keys
|
19
|
+
end
|
20
|
+
|
21
|
+
def with_namespace(namespace)
|
22
|
+
dup.tap { |dupe| dupe.namespace = namespace }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Adds the signature of a dependency to the graph
|
26
|
+
def add(name:, depends_on:, **kwargs)
|
27
|
+
check_for_missing_dependencies!(depends_on)
|
28
|
+
graph[name] = dependency_class.new(kwargs.merge(name: name, depends_on: depends_on))
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def check_for_missing_dependencies!(deps)
|
34
|
+
missing = deps.reject { |dep| graph.key?(dep) }
|
35
|
+
return if missing.empty?
|
36
|
+
|
37
|
+
raise Injectable::MissingDependenciesException, "missing dependencies: #{missing.join(', ')}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Injectable
|
2
|
+
# Initialize a dependency based on the options or the block passed
|
3
|
+
Dependency = Struct.new(:name, :block, :class, :call, :with, :depends_on, keyword_init: true) do
|
4
|
+
def instance(args: [], namespace: nil)
|
5
|
+
positional_args, kwargs = split_args(args)
|
6
|
+
|
7
|
+
wrap_call build_instance(positional_args, kwargs, namespace: namespace)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def split_args(args)
|
13
|
+
args = preprocess_args(args)
|
14
|
+
|
15
|
+
return [[], {}] if args.empty?
|
16
|
+
|
17
|
+
kwargs = args.pop
|
18
|
+
|
19
|
+
if kwargs.is_a?(Hash) && kwargs.keys.all? { |key| key.is_a?(Symbol) }
|
20
|
+
[args, kwargs]
|
21
|
+
else
|
22
|
+
[args << kwargs, {}]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def preprocess_args(args)
|
27
|
+
args = with unless with.nil?
|
28
|
+
wrap_args(args)
|
29
|
+
end
|
30
|
+
|
31
|
+
def wrap_args(args)
|
32
|
+
args.is_a?(Array) ? args.clone : [args]
|
33
|
+
end
|
34
|
+
|
35
|
+
def wrap_call(the_instance)
|
36
|
+
return the_instance unless call
|
37
|
+
|
38
|
+
if the_instance.respond_to? :call
|
39
|
+
raise Injectable::MethodAlreadyExistsException
|
40
|
+
end
|
41
|
+
|
42
|
+
the_instance.public_method(call)
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_instance(args, kwargs, namespace:)
|
46
|
+
return build_instance_26(args, kwargs, namespace: namespace) if RUBY_VERSION < '2.7'
|
47
|
+
|
48
|
+
instantiator = block || klass(namespace: namespace).method(:new)
|
49
|
+
if kwargs.empty?
|
50
|
+
# otherwise an empty hash will be added in ruby 2.7, which could be taken as
|
51
|
+
# positional argument instead.
|
52
|
+
instantiator.call(*args)
|
53
|
+
else
|
54
|
+
instantiator.call(*args, **kwargs)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def klass(namespace:)
|
59
|
+
self.class || resolve(namespace: namespace)
|
60
|
+
end
|
61
|
+
|
62
|
+
def resolve(namespace:)
|
63
|
+
(namespace || Object).const_get(camelcased)
|
64
|
+
end
|
65
|
+
|
66
|
+
def camelcased
|
67
|
+
@camelcased ||= name.to_s.split('_').map(&:capitalize).join
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_instance_26(args, kwargs, namespace:)
|
71
|
+
args << kwargs if kwargs.any?
|
72
|
+
|
73
|
+
block.nil? ? klass(namespace: namespace).new(*args) : block.call(*args)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Injectable
|
2
|
+
module InstanceMethods
|
3
|
+
# Initialize the service with the dependencies injected
|
4
|
+
def initialize(args = {})
|
5
|
+
check_missing_arguments!(self.class.required_initialize_arguments, args)
|
6
|
+
variables_for!(self.class.initialize_arguments, args)
|
7
|
+
variables_from_dependencies!(args)
|
8
|
+
super()
|
9
|
+
end
|
10
|
+
|
11
|
+
# Entry point of the service.
|
12
|
+
# Arguments for this method should be declared explicitly with '.argument'
|
13
|
+
# and declare this method without arguments
|
14
|
+
def call(args = {})
|
15
|
+
check_call_definition!
|
16
|
+
check_missing_arguments!(self.class.required_call_arguments, args)
|
17
|
+
variables_for!(self.class.call_arguments, args)
|
18
|
+
super()
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def instantiate_dependency(name)
|
24
|
+
deps = self.class.dependencies
|
25
|
+
deps[name].instance(args: memoized_dependencies_of(name), namespace: deps.namespace)
|
26
|
+
end
|
27
|
+
|
28
|
+
def memoized_dependencies_of(name)
|
29
|
+
return [] if dependencies_of(name).empty?
|
30
|
+
|
31
|
+
dependencies_of(name).each_with_object({}) { |dep, hash| hash[dep] = public_send(dep) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def dependencies_of(name)
|
35
|
+
self.class.dependencies[name].depends_on
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_call_definition!
|
39
|
+
return if (self.class.ancestors - [Injectable::InstanceMethods]).any? do |ancestor|
|
40
|
+
ancestor.instance_methods(false).include?(:call)
|
41
|
+
end
|
42
|
+
raise NoMethodError, "A #call method with zero arity must be defined in #{self.class}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def check_missing_arguments!(expected, args)
|
46
|
+
missing = expected - args.keys
|
47
|
+
return if missing.empty?
|
48
|
+
raise ArgumentError, "missing keywords: #{missing.join(',')}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def variables_for!(subject, args)
|
52
|
+
subject.each do |arg, options|
|
53
|
+
instance_variable_set("@#{arg}", args.fetch(arg) { options[:default] })
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def variables_from_dependencies!(args)
|
58
|
+
self.class.dependencies.names.each do |name|
|
59
|
+
next if self.class.initialize_arguments.key?(name)
|
60
|
+
next unless args.key?(name)
|
61
|
+
next instance_variable_set("@#{name}", args[name]) unless args[name].respond_to?(:new)
|
62
|
+
next instance_variable_set("@#{name}", args[name].new) if dependencies_of(name).empty?
|
63
|
+
|
64
|
+
instance_variable_set("@#{name}", args[name].new(memoized_dependencies_of(name)))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/injectable.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'injectable/version'
|
2
|
+
require 'injectable/class_methods'
|
3
|
+
require 'injectable/dependencies_graph'
|
4
|
+
require 'injectable/dependency'
|
5
|
+
require 'injectable/instance_methods'
|
6
|
+
require 'injectable/missing_dependencies_exception'
|
7
|
+
require 'injectable/method_already_exists_exception'
|
8
|
+
|
9
|
+
# Convert your class into an injectable service
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# You would create a service like this:
|
13
|
+
#
|
14
|
+
# class AddPlayerToTeamRoster
|
15
|
+
# include Injectable
|
16
|
+
#
|
17
|
+
# dependency :team_query
|
18
|
+
# dependency :player_query, class: UserQuery
|
19
|
+
#
|
20
|
+
# argument :team_id
|
21
|
+
# argument :player_id
|
22
|
+
#
|
23
|
+
# def call
|
24
|
+
# player_must_exist!
|
25
|
+
# team_must_exist!
|
26
|
+
# team_must_accept_players!
|
27
|
+
#
|
28
|
+
# team.add_to_roster(player)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# private
|
32
|
+
#
|
33
|
+
# def player
|
34
|
+
# @player ||= player_query.call(player_id)
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# def team
|
38
|
+
# @team ||= team_query.call(team_id)
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# def player_must_exist!
|
42
|
+
# player.present? || raise UserNotFoundException
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# def team_must_exist!
|
46
|
+
# team.present? || raise TeamNotFoundException
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# def team_must_accept_players!
|
50
|
+
# team.accepts_players? || raise TeamFullException
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# And use it like this:
|
55
|
+
#
|
56
|
+
# AddPlayerToTeamRoster.call(player_id: player.id, team_id: team.id)
|
57
|
+
module Injectable
|
58
|
+
def self.included(base)
|
59
|
+
base.extend(Injectable::ClassMethods)
|
60
|
+
base.prepend(Injectable::InstanceMethods)
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'injectable/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'maglev-injectable'
|
7
|
+
spec.version = Injectable::VERSION
|
8
|
+
spec.authors = %w[Papipo iovis jantequera amrocco rewritten]
|
9
|
+
spec.email = %w[dev@rubiconmd.com]
|
10
|
+
|
11
|
+
spec.summary = 'A library to help you build nice service objects with dependency injection.'
|
12
|
+
spec.homepage = 'https://github.com/rubiconmd/injectable'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
# Specify which files should be added to the gem when it is released.
|
16
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
17
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
18
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
19
|
+
end
|
20
|
+
spec.bindir = 'exe'
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
spec.required_ruby_version = '>= 2.5'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
26
|
+
spec.add_development_dependency 'pry-byebug'
|
27
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: maglev-injectable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Papipo
|
8
|
+
- iovis
|
9
|
+
- jantequera
|
10
|
+
- amrocco
|
11
|
+
- rewritten
|
12
|
+
autorequire:
|
13
|
+
bindir: exe
|
14
|
+
cert_chain: []
|
15
|
+
date: 2022-09-27 00:00:00.000000000 Z
|
16
|
+
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
18
|
+
name: bundler
|
19
|
+
requirement: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - "~>"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '2.0'
|
24
|
+
type: :development
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - "~>"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '2.0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: pry-byebug
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: rake
|
47
|
+
requirement: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - "~>"
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '13.0'
|
52
|
+
type: :development
|
53
|
+
prerelease: false
|
54
|
+
version_requirements: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - "~>"
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '13.0'
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: rspec
|
61
|
+
requirement: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - "~>"
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '3.0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - "~>"
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '3.0'
|
73
|
+
description:
|
74
|
+
email:
|
75
|
+
- dev@rubiconmd.com
|
76
|
+
executables: []
|
77
|
+
extensions: []
|
78
|
+
extra_rdoc_files: []
|
79
|
+
files:
|
80
|
+
- ".github/workflows/ruby.yml"
|
81
|
+
- ".gitignore"
|
82
|
+
- ".rspec"
|
83
|
+
- ".travis.yml"
|
84
|
+
- CHANGELOG.md
|
85
|
+
- CODE_OF_CONDUCT.md
|
86
|
+
- Gemfile
|
87
|
+
- Gemfile.lock
|
88
|
+
- LICENSE.txt
|
89
|
+
- README.md
|
90
|
+
- Rakefile
|
91
|
+
- bin/console
|
92
|
+
- bin/setup
|
93
|
+
- lib/injectable.rb
|
94
|
+
- lib/injectable/class_methods.rb
|
95
|
+
- lib/injectable/dependencies_graph.rb
|
96
|
+
- lib/injectable/dependency.rb
|
97
|
+
- lib/injectable/instance_methods.rb
|
98
|
+
- lib/injectable/method_already_exists_exception.rb
|
99
|
+
- lib/injectable/missing_dependencies_exception.rb
|
100
|
+
- lib/injectable/version.rb
|
101
|
+
- maglev-injectable.gemspec
|
102
|
+
homepage: https://github.com/rubiconmd/injectable
|
103
|
+
licenses:
|
104
|
+
- MIT
|
105
|
+
metadata: {}
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '2.5'
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
requirements: []
|
121
|
+
rubygems_version: 3.1.6
|
122
|
+
signing_key:
|
123
|
+
specification_version: 4
|
124
|
+
summary: A library to help you build nice service objects with dependency injection.
|
125
|
+
test_files: []
|