shirinji 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +35 -0
- data/README.md +307 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/shirinji/attribute.rb +12 -0
- data/lib/shirinji/bean.rb +35 -0
- data/lib/shirinji/map.rb +129 -0
- data/lib/shirinji/resolver.rb +72 -0
- data/lib/shirinji/scope.rb +45 -0
- data/lib/shirinji/version.rb +5 -0
- data/lib/shirinji.rb +12 -0
- data/shirinji.gemspec +28 -0
- metadata +105 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 41ac63bcf3f6fd45d64dbcf12eafde69547eba1d
|
4
|
+
data.tar.gz: c07d6da743a36f3800f8a7a29bc3c8c6216368f1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8060030b8a19b19d5b7abdc90fe8d9c1721851a2ea7c867d42ddb509f3d0d41e86865816a8caa5e1a61c10158be4aa7128be08a9a01384b48d7923e7dbbde133
|
7
|
+
data.tar.gz: caf8ca03a33a67c8feb56c9dd5fd57ba20b472dadc7930f00f3b565dad71e5beba3fba97948cf109dec20321b425c2e3af4d1173e90be887ecc4810368d0b263
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
shirinji
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.4
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
shirinji (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.3)
|
10
|
+
rake (10.5.0)
|
11
|
+
rspec (3.7.0)
|
12
|
+
rspec-core (~> 3.7.0)
|
13
|
+
rspec-expectations (~> 3.7.0)
|
14
|
+
rspec-mocks (~> 3.7.0)
|
15
|
+
rspec-core (3.7.0)
|
16
|
+
rspec-support (~> 3.7.0)
|
17
|
+
rspec-expectations (3.7.0)
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
19
|
+
rspec-support (~> 3.7.0)
|
20
|
+
rspec-mocks (3.7.0)
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
22
|
+
rspec-support (~> 3.7.0)
|
23
|
+
rspec-support (3.7.0)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
bundler (~> 1.16)
|
30
|
+
rake (~> 10.0)
|
31
|
+
rspec (~> 3.0)
|
32
|
+
shirinji!
|
33
|
+
|
34
|
+
BUNDLED WITH
|
35
|
+
1.16.1
|
data/README.md
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
# Shirinji
|
2
|
+
|
3
|
+
Container manager for dependency injection in Ruby.
|
4
|
+
|
5
|
+
## Principles
|
6
|
+
|
7
|
+
Dependencies Injection is strongly connected with the IOC (inversion of controls) pattern. IOC is
|
8
|
+
often seen as a "Java thing" and tend to be rejected by Ruby community.
|
9
|
+
|
10
|
+
Yet, it's heavily used in javascript world and fit perfectly with prototyped language.
|
11
|
+
|
12
|
+
```javascript
|
13
|
+
function updateUI(evt) { /* ... */ }
|
14
|
+
|
15
|
+
$.ajax('/action', { onSuccess: updateUI, ... })
|
16
|
+
```
|
17
|
+
|
18
|
+
A simple script like that is very common in Javascript and nobody is shocked by that. Yet, it's
|
19
|
+
using the IOC pattern. The `$.ajax` method is delegating the action to perform when the request
|
20
|
+
is successful to something else, focusing only on handling the http communication part.
|
21
|
+
|
22
|
+
Dependencies injection is nothing more than the exact same principle but applied to objects instead
|
23
|
+
of functions.
|
24
|
+
|
25
|
+
Let's follow an example step by step from "the rails way" to a proper way to understand it better.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class User < ActiveRecord::Base
|
29
|
+
after_create :publish_statistics, :send_confirmation_email
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def publish_statistics
|
34
|
+
StatisticsGateway.publish_event(:new_user, user.id)
|
35
|
+
end
|
36
|
+
|
37
|
+
def send_confirmation_email
|
38
|
+
UserMailer.confirm_email(user).deliver
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
This is called "the rails way" and everybody with a tiny bit of experience knows that this way
|
44
|
+
is not valid. Your model is gonna send statistics and emails each time it's saved, even when it's
|
45
|
+
not in the context of signing up a new user (confusion between sign up, a business level operation
|
46
|
+
and create, a persistency level operation). There are plenty of situation where you actually don't
|
47
|
+
want those operations to be performed (db seeding, imports, fixtures in tests ...)
|
48
|
+
|
49
|
+
That's where services pattern comes to the rescue. Let's do it in a very simple fashion and just
|
50
|
+
move everything "as it is" in a service.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class SignUpUserService
|
54
|
+
def call(user)
|
55
|
+
user.signed_up_at = Time.now
|
56
|
+
user.save!
|
57
|
+
StatisticsGateway.publish_event(:new_user, user.id)
|
58
|
+
UserMailer.confirm_email(user).deliver
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
## test
|
63
|
+
|
64
|
+
RSpec.describe SignUpUserService do
|
65
|
+
let(:service) { described_class.new }
|
66
|
+
|
67
|
+
describe '.call' do
|
68
|
+
let(:message_instance) { double(deliver: nil) }
|
69
|
+
let(:user) { FactoryGirl.build_stubbed(:user, id: 1) }
|
70
|
+
|
71
|
+
before do
|
72
|
+
allow(StatisticsGateway).to receive(:publish_event)
|
73
|
+
allow(UserMailer).to receive(:confirm_email).and_return(message_instance)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'saves user' do
|
77
|
+
expect(user).to receive(:save!)
|
78
|
+
|
79
|
+
service.call(user)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'sets signed up time' do
|
83
|
+
service.call(user)
|
84
|
+
expect(user.signed_up_at).to_not be_nil
|
85
|
+
# there are better ways to test that but we don't care here
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'publishes statistics' do
|
89
|
+
expect(StatisticsGateway).to receive(:publish_event).with(:new_user, 1)
|
90
|
+
|
91
|
+
service.call(user)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'notifies user for identity confirmation' do
|
95
|
+
expect(UserMailer).to receive(:confirm_email).with(user)
|
96
|
+
expect(message_instance).to receive(:deliver)
|
97
|
+
|
98
|
+
service.call(user)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
It's a bit better. Now when we want to write a user in DB, it's not acting as a signup regardless
|
105
|
+
the context. It will act as a sign up only when we call SignUpService.
|
106
|
+
|
107
|
+
Yet, if we look a the tests for this service, we have to mock `StatisticsGateway` and
|
108
|
+
`UserMailer` in order for the test to run properly. It means that we need a very precise knowledge
|
109
|
+
of the implementation, and we need to mock global static objects which can be a very big problem
|
110
|
+
(for example, if the same class is called twice in very different contexts in the same method)
|
111
|
+
|
112
|
+
Also, if we decide to switch our statistics solution, or if we decide to change the way we notify
|
113
|
+
users for identity confirmation, our test for signing up a user will have to change.
|
114
|
+
It shouldn't. The way we sign up users should not change according to the solution we chose to
|
115
|
+
send emails.
|
116
|
+
|
117
|
+
This demonstrate that our object has too many responsibilities. If you want to write efficient,
|
118
|
+
fast, scalable, readable ... code, you should restrict your objects to one and only one responsbility.
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class SignUpUserService
|
122
|
+
def call(user)
|
123
|
+
user.signed_up_at = Time.now
|
124
|
+
user.save!
|
125
|
+
|
126
|
+
PublishUserStatisticsService.new.call(user)
|
127
|
+
SendUserEmailConfirmationService.new.call(user)
|
128
|
+
# implementation omitted for those services, you can figure it out
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
Now, our service has fewer responsibilities BUT, testing will be even harder because mocking `new`
|
134
|
+
method on both "sub services" will be even more dirty than before.
|
135
|
+
We can solve this problem very easily
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
class SignUpUserService
|
139
|
+
def call(user)
|
140
|
+
user.signed_up_at = Time.now
|
141
|
+
user.save!
|
142
|
+
|
143
|
+
publish_user_statistics_service.call(user)
|
144
|
+
send_user_email_confirmation_service.call(user)
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def publish_user_statistics_service
|
150
|
+
PublishUserStatisticsService.new
|
151
|
+
end
|
152
|
+
|
153
|
+
def send_user_email_confirmation_service
|
154
|
+
SendUserEmailConfirmationService.new
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
## test
|
159
|
+
|
160
|
+
RSpec.describe SignUpUserService do
|
161
|
+
let(:publish_statistics_service) { double(call: nil) }
|
162
|
+
let(:send_email_confirmation_service) { double(call: nil) }
|
163
|
+
|
164
|
+
let(:service) { described_class.new }
|
165
|
+
|
166
|
+
before do
|
167
|
+
allow(service).to receive(:publish_user_statistics_service).and_return(publish_statistics_service)
|
168
|
+
allow(service).to receive(:send_user_email_confirmation_service).and_return(send_email_confirmation_service)
|
169
|
+
end
|
170
|
+
|
171
|
+
# ...
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
Our tests are now much easier to write. They're also much faster because our test is very specialized
|
176
|
+
and focus only on the service itself.
|
177
|
+
But if you think about it, this service still has too many responsibilities. It still carrying the
|
178
|
+
responsibility of choosing which service will execute the "sub tasks" and more important, it's in
|
179
|
+
charge of creating those services instances.
|
180
|
+
|
181
|
+
Instead of having strong dependencies to other services, we can make them "weak" and increase our
|
182
|
+
code flexibility if we want to reuse it in another project.
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
class SignUpUserService
|
186
|
+
attr_reader :publish_user_statistics_service,
|
187
|
+
:send_user_email_confirmation_service
|
188
|
+
|
189
|
+
def initialize(
|
190
|
+
publish_user_statistics_service:,
|
191
|
+
send_user_email_confirmation_service:,
|
192
|
+
)
|
193
|
+
@publish_user_statistics_service = publish_user_statistics_service
|
194
|
+
@send_user_email_confirmation_service = send_user_email_confirmation_service
|
195
|
+
end
|
196
|
+
|
197
|
+
def call(user)
|
198
|
+
user.signed_up_at = Time.now
|
199
|
+
user.save!
|
200
|
+
|
201
|
+
publish_user_statistics_service.call(user)
|
202
|
+
send_user_email_confirmation_service.call(user)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
```
|
206
|
+
|
207
|
+
Now our service is completely agnostic about which solution is used to perform the "sub tasks".
|
208
|
+
It's the responsibility of it's environment to provide this information.
|
209
|
+
|
210
|
+
But in a real world example, building such a tree is a complete nightmare and impossible to
|
211
|
+
maintain. It's where Shiringji comes to the rescue.
|
212
|
+
|
213
|
+
## Usage
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
map = Shirinji::Map.new do
|
217
|
+
bean(:sign_up_user_service, klass: "SignUpUserService")
|
218
|
+
bean(:publish_user_statistics_service, klass: "PublishUserStatisticsService")
|
219
|
+
bean(:send_user_email_confirmation_service, klass: "SendUserEmailConfirmationService")
|
220
|
+
end
|
221
|
+
|
222
|
+
resolver = Shirinji::Resolver.new(map)
|
223
|
+
|
224
|
+
resolver.resolve(:sign_up_user_service)
|
225
|
+
#=> <#SignUpUserService @publish_user_statistics_service=<#PublishUserStatisticsService ...> ...>
|
226
|
+
```
|
227
|
+
|
228
|
+
In this example, because `SingUpUserService` constructor parameters match beans with the same name,
|
229
|
+
Shirinji will automatically resolve them.
|
230
|
+
|
231
|
+
In a case where a parameter name match no bean, it has to be mapped explicitly.
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
map = Shirinji::Map.new do
|
235
|
+
bean(:sign_up_user_service, klass: "SignUpUserService") do
|
236
|
+
attr :publish_user_statistics_service, ref: :user_publish_statistics_service
|
237
|
+
end
|
238
|
+
|
239
|
+
# note the name is different
|
240
|
+
bean(:user_publish_statistics_service, klass: "PublishUserStatisticsService")
|
241
|
+
bean(:send_user_email_confirmation_service, klass: "SendUserEmailConfirmationService")
|
242
|
+
end
|
243
|
+
|
244
|
+
resolver = Shirinji::Resolver.new(map)
|
245
|
+
|
246
|
+
resolver.resolve(:sign_up_user_service)
|
247
|
+
#=> <#SignUpUserService @publish_user_statistics_service=<#PublishUserStatisticsService ...> ...>
|
248
|
+
```
|
249
|
+
|
250
|
+
Shirinji also provide a caching mecanism to achieve singleton pattern without having to implement
|
251
|
+
the pattern in your classes. It means the same class can be used as a singleton AND a regular class
|
252
|
+
at the same time without any code change.
|
253
|
+
|
254
|
+
Singleton is the default access mode for a bean.
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
map = Shirinji::Map.new do
|
258
|
+
bean(:foo, klass: 'Foo', access: :singleton) # foo is singleton
|
259
|
+
bean(:bar, klass: 'Bar', access: :instance) # bar is not
|
260
|
+
end
|
261
|
+
|
262
|
+
resolver = Shirinji::Resolver.new(map)
|
263
|
+
|
264
|
+
resolver.resolve(:foo).object_id #=> 1
|
265
|
+
resolver.resolve(:foo).object_id #=> 1
|
266
|
+
|
267
|
+
resolver.resolve(:bar).object_id #=> 2
|
268
|
+
resolver.resolve(:bar).object_id #=> 3
|
269
|
+
```
|
270
|
+
|
271
|
+
You can also create beans that contain single values. It will help you to avoid referencing global
|
272
|
+
variables in your code.
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
map = Shirinji::Map.new do
|
276
|
+
bean(:config, value: Proc.new { Application.config })
|
277
|
+
bean(:foo, klass: 'Foo')
|
278
|
+
end
|
279
|
+
|
280
|
+
resolver = Shirinji::Resolver.new(map)
|
281
|
+
|
282
|
+
class Foo
|
283
|
+
attr_reader :config
|
284
|
+
|
285
|
+
def initialize(config:)
|
286
|
+
@config = config
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
resolver.resolve(:foo)
|
291
|
+
#=> <#Foo @config=<#OpenStruct ...> ...>
|
292
|
+
```
|
293
|
+
|
294
|
+
Values can be anything. A `Proc` will be lazily evaluated. They also obey the singleton / instance
|
295
|
+
strategy.
|
296
|
+
|
297
|
+
## Notes
|
298
|
+
|
299
|
+
- It is absolutely mandatory for your beans to be stateless to use the singleton mode. If they're
|
300
|
+
not, you will probably run into trouble as your objects behavior will depend on their history, leading
|
301
|
+
to unpredictable effects.
|
302
|
+
- Shirinji only works with named arguments. It will raise errors if you try to use it with "standard"
|
303
|
+
method arguments.
|
304
|
+
|
305
|
+
## Contributing
|
306
|
+
|
307
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/fdutey/shirinji.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'shirinji'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shirinji
|
4
|
+
class Bean
|
5
|
+
attr_reader :name, :class_name, :value, :access, :attributes
|
6
|
+
|
7
|
+
def initialize(name, class_name: nil, value: nil, access:, &block)
|
8
|
+
check_params!(class_name, value)
|
9
|
+
|
10
|
+
@name = name
|
11
|
+
@class_name = class_name
|
12
|
+
@value = value
|
13
|
+
@access = access
|
14
|
+
@attributes = {}
|
15
|
+
|
16
|
+
instance_eval(&block) if block
|
17
|
+
end
|
18
|
+
|
19
|
+
def attr(name, ref:)
|
20
|
+
attributes[name] = Attribute.new(name, ref)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def check_params!(class_name, value)
|
26
|
+
msg = if class_name && value
|
27
|
+
'you can use either `class_name` or `value` but not both'
|
28
|
+
elsif !class_name && !value
|
29
|
+
'you must pass either `class_name` or `value`'
|
30
|
+
end
|
31
|
+
|
32
|
+
raise ArgumentError, msg if msg
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/shirinji/map.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shirinji
|
4
|
+
class Map
|
5
|
+
attr_reader :beans
|
6
|
+
|
7
|
+
def initialize(&block)
|
8
|
+
@beans = {}
|
9
|
+
|
10
|
+
instance_eval(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns a bean based on its name
|
14
|
+
#
|
15
|
+
# @example accessing a bean
|
16
|
+
# map.get(:foo)
|
17
|
+
# #=> <#Shirinji::Bean ....>
|
18
|
+
#
|
19
|
+
# @example accessing a bean that doesn't exist
|
20
|
+
# map.get(:bar)
|
21
|
+
# #=> raises ArgumentError (unknown bean)
|
22
|
+
#
|
23
|
+
# @param name [Symbol, String] the name of the bean you want to access to
|
24
|
+
# @return [Bean] A bean with the given name or raises an error
|
25
|
+
# @raise [ArgumentError] if trying to access a bean that doesn't exist
|
26
|
+
def get(name)
|
27
|
+
bean = beans[name.to_sym]
|
28
|
+
raise ArgumentError, "Unknown bean #{name}" unless bean
|
29
|
+
bean
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add a bean to the map
|
33
|
+
#
|
34
|
+
# @example build a class bean
|
35
|
+
# map.bean(:foo, klass: 'Foo', access: :singleton)
|
36
|
+
#
|
37
|
+
# @example build a class bean with attributes
|
38
|
+
# map.bean(:foo, klass: 'Foo', access: :singleton) do
|
39
|
+
# attr :bar, ref: :baz
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# @example build a value bean
|
43
|
+
# map.bean(:bar, value: 5)
|
44
|
+
#
|
45
|
+
# @example build a lazy evaluated value bean
|
46
|
+
# map.bean(:bar, value: Proc.new { 5 })
|
47
|
+
#
|
48
|
+
# @param name [Symbol] the name you want to register your bean
|
49
|
+
# @option [String] :klass the classname the bean is registering
|
50
|
+
# @option [*] :value the object registered by the bean
|
51
|
+
# @option [Symbol] :access either :singleton or :instance.
|
52
|
+
# @yield additional method to construct our bean
|
53
|
+
# @raise [ArgumentError] if trying to register a bean that already exist
|
54
|
+
def bean(name, klass: nil, value: nil, access: :singleton, &block)
|
55
|
+
name = name.to_sym
|
56
|
+
|
57
|
+
raise_if_name_already_taken!(name)
|
58
|
+
|
59
|
+
options = {
|
60
|
+
access: access,
|
61
|
+
class_name: klass ? klass.freeze : nil,
|
62
|
+
value: value
|
63
|
+
}
|
64
|
+
|
65
|
+
beans[name] = Bean.new(name, **options, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Scopes a given set of bean to the default options
|
69
|
+
#
|
70
|
+
# @example module
|
71
|
+
# scope(module: :Foo) do
|
72
|
+
# bean(:bar, klass: 'Bar')
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# #=> bean(:bar, klass: 'Foo::Bar')
|
76
|
+
#
|
77
|
+
# @example prefix
|
78
|
+
# scope(prefix: :foo) do
|
79
|
+
# bean(:bar, klass: 'Bar')
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# #=> bean(:foo_bar, klass: 'Bar')
|
83
|
+
#
|
84
|
+
# @example suffix
|
85
|
+
# scope(suffix: :bar) do
|
86
|
+
# bean(:foo, klass: 'Foo')
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# #=> bean(:foo_bar, klass: 'Foo')
|
90
|
+
#
|
91
|
+
# @example class suffix
|
92
|
+
# scope(klass_suffix: :Bar) do
|
93
|
+
# bean(:foo, klass: 'Foo')
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# #=> bean(:foo, klass: 'FooBar')
|
97
|
+
#
|
98
|
+
# It comes pretty handy when used with strongly normative naming
|
99
|
+
#
|
100
|
+
# @example services
|
101
|
+
# scope(module: :Services, klass_suffix: :Service, suffix: :service) do
|
102
|
+
# scope(module: :User, prefix: :user) do
|
103
|
+
# bean(:signup, klass: 'Signup')
|
104
|
+
# bean(:ban, klass: 'Ban')
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# #=> bean(:user_signup_service, klass: 'Services::User::SignupService')
|
109
|
+
# #=> bean(:user_ban_service, klass: 'Services::User::BanService')
|
110
|
+
#
|
111
|
+
# @param options [Hash]
|
112
|
+
# @option options [Symbol] :module prepend module name to class name
|
113
|
+
# @option options [Symbol] :prefix prepend prefix to bean name
|
114
|
+
# @option options [Symbol] :suffix append suffix to bean name
|
115
|
+
# @option options [Symbol] :klass_suffix append suffix to class name
|
116
|
+
# @yield a standard map
|
117
|
+
def scope(**options, &block)
|
118
|
+
Scope.new(self, **options, &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def raise_if_name_already_taken!(name)
|
124
|
+
return unless beans[name]
|
125
|
+
msg = "A bean already exists with the following name: #{name}"
|
126
|
+
raise ArgumentError, msg
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shirinji
|
4
|
+
class Resolver
|
5
|
+
ARG_TYPES = %i[key keyreq].freeze
|
6
|
+
|
7
|
+
attr_reader :map, :singletons
|
8
|
+
|
9
|
+
def initialize(map)
|
10
|
+
@map = map
|
11
|
+
@singletons = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve(name)
|
15
|
+
bean = map.get(name)
|
16
|
+
|
17
|
+
if bean.access == :singleton
|
18
|
+
single = singletons[name]
|
19
|
+
return single if single
|
20
|
+
end
|
21
|
+
|
22
|
+
resolve_bean(bean).tap do |instance|
|
23
|
+
singletons[name] = instance if bean.access == :singleton
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset_cache
|
28
|
+
@singletons = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def resolve_bean(bean)
|
34
|
+
send(:"resolve_#{bean.value ? :value : :class}_bean", bean)
|
35
|
+
end
|
36
|
+
|
37
|
+
def resolve_value_bean(bean)
|
38
|
+
bean.value.is_a?(Proc) ? bean.value.call : bean.value
|
39
|
+
end
|
40
|
+
|
41
|
+
def resolve_class_bean(bean)
|
42
|
+
klass, params = resolve_class(bean)
|
43
|
+
return klass.new if params.empty?
|
44
|
+
|
45
|
+
check_params!(params)
|
46
|
+
|
47
|
+
args = params.each_with_object({}) do |(_type, arg), memo|
|
48
|
+
memo[arg] = bean(resolve_attribute(bean, arg))
|
49
|
+
end
|
50
|
+
|
51
|
+
klass.new(**args)
|
52
|
+
end
|
53
|
+
|
54
|
+
def resolve_class(bean)
|
55
|
+
klass = bean.class_name.constantize
|
56
|
+
construct = klass.instance_method(:initialize)
|
57
|
+
|
58
|
+
[klass, construct.parameters]
|
59
|
+
end
|
60
|
+
|
61
|
+
def resolve_attribute(bean, arg)
|
62
|
+
(attr = bean.attributes[arg]) ? attr.reference : arg
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_params!(params)
|
66
|
+
params.each do |pair|
|
67
|
+
next if ARG_TYPES.include?(pair.first)
|
68
|
+
raise ArgumentError, 'Only key arguments are allowed'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Shirinji
|
4
|
+
class Scope
|
5
|
+
VALID_OPTIONS = %i[module prefix suffix klass_suffix].freeze
|
6
|
+
|
7
|
+
attr_reader :parent, :mod, :prefix, :suffix, :klass_suffix
|
8
|
+
|
9
|
+
def initialize(parent, **options, &block)
|
10
|
+
validate_options(options)
|
11
|
+
|
12
|
+
@parent = parent
|
13
|
+
@mod = options[:module]
|
14
|
+
@prefix = options[:prefix]
|
15
|
+
@suffix = options[:suffix]
|
16
|
+
@klass_suffix = options[:klass_suffix]
|
17
|
+
|
18
|
+
instance_eval(&block) if block
|
19
|
+
end
|
20
|
+
|
21
|
+
def bean(name, klass: nil, value: nil, access: :singleton, &block)
|
22
|
+
chunks = [mod, "#{klass}#{klass_suffix}"].compact
|
23
|
+
options = {
|
24
|
+
access: access,
|
25
|
+
klass: klass ? chunks.join('::') : nil,
|
26
|
+
value: value
|
27
|
+
}
|
28
|
+
|
29
|
+
parent.bean([prefix, name, suffix].compact.join('_'), **options, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def scope(**options, &block)
|
33
|
+
Scope.new(self, **options, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def validate_options(args)
|
39
|
+
args.each_key do |k|
|
40
|
+
next if Shirinji::Scope::VALID_OPTIONS.include?(k)
|
41
|
+
raise ArgumentError, "Unknown key #{k}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/shirinji.rb
ADDED
data/shirinji.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path('../lib', __FILE__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require 'shirinji/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'shirinji'
|
10
|
+
spec.version = Shirinji::VERSION
|
11
|
+
spec.authors = ['Florian Dutey']
|
12
|
+
spec.email = ['fdutey@gmail.com']
|
13
|
+
|
14
|
+
spec.summary = 'Dependencies injection made easy'
|
15
|
+
spec.description = 'Dependencies injections made easy for Ruby'
|
16
|
+
spec.homepage = 'https://github.com/fdutey/shirinji'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
26
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shirinji
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Florian Dutey
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-02-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
description: Dependencies injections made easy for Ruby
|
56
|
+
email:
|
57
|
+
- fdutey@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- ".rubocop.yml"
|
65
|
+
- ".ruby-gemset"
|
66
|
+
- ".ruby-version"
|
67
|
+
- ".travis.yml"
|
68
|
+
- Gemfile
|
69
|
+
- Gemfile.lock
|
70
|
+
- README.md
|
71
|
+
- Rakefile
|
72
|
+
- bin/console
|
73
|
+
- bin/setup
|
74
|
+
- lib/shirinji.rb
|
75
|
+
- lib/shirinji/attribute.rb
|
76
|
+
- lib/shirinji/bean.rb
|
77
|
+
- lib/shirinji/map.rb
|
78
|
+
- lib/shirinji/resolver.rb
|
79
|
+
- lib/shirinji/scope.rb
|
80
|
+
- lib/shirinji/version.rb
|
81
|
+
- shirinji.gemspec
|
82
|
+
homepage: https://github.com/fdutey/shirinji
|
83
|
+
licenses: []
|
84
|
+
metadata: {}
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 2.6.14
|
102
|
+
signing_key:
|
103
|
+
specification_version: 4
|
104
|
+
summary: Dependencies injection made easy
|
105
|
+
test_files: []
|