shirinji 0.0.2 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 9e72d17d6ce2db50ed67b5ae251a372ad202620a
4
- data.tar.gz: c78b751572c478329040fb289fb405253e921f46
2
+ SHA256:
3
+ metadata.gz: 6dc426a5294d566ede0acdf7b46568d09736ef05d044afbed686bb604cd48830
4
+ data.tar.gz: 2fc85bcb5c7a1c5050cc6a29397e3b2380b89482442f64e119676c463670ffeb
5
5
  SHA512:
6
- metadata.gz: f7efa6c82ec9558e7d89ca90bc26e5639d2fecc9d31dc1ef4d1bd3c0ff3e12b5f59f744a172633f5c02ed29e2812dbe8861e9cf78461d190795b74bc066bef07
7
- data.tar.gz: 9a445d8d26e846184318eb37a4fd824a628f5343f854f4cd7bace5b5aa01a3e1529379f6ff2f1293c556e92db513e5136e95b134a28a99e5500175d9547783f7
6
+ metadata.gz: 7f8cb9af67898e849acd3bc6c1885094dea834db218f5ab6a39b167ebb54d8ea3d6371c62692a83a27c62124714c2762087a7c79e55821717852f22835e0bad9
7
+ data.tar.gz: 07e1eac7f60615db3ecd869d59d3a648c269d56926f09a8ca5897f618d3a69b2849508c9887623ac12e2f04957fed777b01ec77dd1cd666c674ad1bfd003f856
data/.codeclimate.yml ADDED
@@ -0,0 +1,3 @@
1
+ plugins:
2
+ rubocop:
3
+ enabled: true
data/.gitignore CHANGED
@@ -9,4 +9,7 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
- .idea
12
+ .idea
13
+
14
+ docker-compose.yml
15
+ Dockerfile
data/.travis.yml CHANGED
@@ -1,5 +1,38 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.4
5
- before_install: gem install bundler -v 1.16.0
4
+ - 2.4.0
5
+ - 2.4.1
6
+ - 2.4.2
7
+ - 2.4.3
8
+ - 2.4.4
9
+ - 2.4.5
10
+ - 2.4.6
11
+ - 2.4.7
12
+ - 2.4.8
13
+ - 2.4.9
14
+ - 2.5.0
15
+ - 2.5.1
16
+ - 2.5.2
17
+ - 2.5.3
18
+ - 2.5.4
19
+ - 2.5.5
20
+ - 2.5.6
21
+ - 2.5.7
22
+ - 2.5.8
23
+ - 2.5.9
24
+ - 2.6.0
25
+ - 2.6.1
26
+ - 2.6.2
27
+ - 2.6.3
28
+ - 2.6.4
29
+ - 2.6.5
30
+ - 2.6.6
31
+ - 2.6.7
32
+ - 2.7.0
33
+ - 2.7.1
34
+ - 2.7.2
35
+ - 2.7.3
36
+ - 3.0.0
37
+ - 3.0.1
38
+ before_install: gem install bundler
@@ -0,0 +1,10 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ Speech is free, you can tell anything you want.
6
+
7
+ ## Our Standards
8
+
9
+ Sue me.
10
+
data/Gemfile.lock CHANGED
@@ -1,13 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shirinji (0.0.1)
4
+ shirinji (0.0.7)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  diff-lcs (1.3)
10
- rake (10.5.0)
10
+ rake (13.0.3)
11
11
  rspec (3.7.0)
12
12
  rspec-core (~> 3.7.0)
13
13
  rspec-expectations (~> 3.7.0)
@@ -26,10 +26,9 @@ PLATFORMS
26
26
  ruby
27
27
 
28
28
  DEPENDENCIES
29
- bundler (~> 1.16)
30
- rake (~> 10.0)
29
+ rake (>= 12.3.3)
31
30
  rspec (~> 3.0)
32
31
  shirinji!
33
32
 
34
33
  BUNDLED WITH
35
- 1.16.1
34
+ 2.2.16
data/README.md CHANGED
@@ -1,307 +1,337 @@
1
1
  # Shirinji
2
2
 
3
- Container manager for dependency injection in Ruby.
3
+ [![Gem Version](https://badge.fury.io/rb/shirinji.svg)](
4
+ https://badge.fury.io/rb/shirinji
5
+ )
6
+ [![Build Status](https://travis-ci.org/fdutey/shirinji.svg?branch=master)](
7
+ https://travis-ci.org/fdutey/shirinji
8
+ )
9
+ [![Maintainability](
10
+ https://api.codeclimate.com/v1/badges/4b1c0010788d70581680/maintainability)
11
+ ](https://codeclimate.com/github/fdutey/shirinji/maintainability)
12
+
13
+ Dependencies Injection made clean and easy for Ruby.
14
+
15
+ ## Supported ruby versions
16
+
17
+ - 2.4.x
18
+ - 2.5.x
19
+ - 2.6.x
20
+ - 2.7.x
21
+ - 3.0.x
4
22
 
5
23
  ## Principles
6
24
 
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.
25
+ Remove hard dependencies between your objects and delegate object tree building
26
+ to an unobtrusive framework with cool convention over configuration.
9
27
 
10
- Yet, it's heavily used in javascript world and fit perfectly with prototyped language.
28
+ Shirinji relies on a mapping of beans and a resolver. When you resolve a bean,
29
+ it will return (by default) an instance of the class associated to the bean,
30
+ with all the bean dependencies resolved.
11
31
 
12
- ```javascript
13
- function updateUI(evt) { /* ... */ }
32
+ ```ruby
33
+ class FooService
34
+ attr_reader :bar_service
35
+
36
+ def initialize(bar_service:)
37
+ @bar_service = bar_service
38
+ end
39
+
40
+ def call(obj)
41
+ obj.foo = 123
42
+
43
+ bar_service.call(obj)
44
+ end
45
+ end
14
46
 
15
- $.ajax('/action', { onSuccess: updateUI, ... })
16
- ```
47
+ map = Shirinji::Map.new do
48
+ bean(:foo_service, klass: 'FooService')
49
+ bean(:bar_service, klass: 'BarService')
50
+ end
17
51
 
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.
52
+ resolver = Shirinji::Resolver.new(map)
21
53
 
22
- Dependencies injection is nothing more than the exact same principle but applied to objects instead
23
- of functions.
54
+ resolver.resolve(:foo_service)
55
+ # => <#FooService @bar_service=<#BarService>>
56
+ ```
24
57
 
25
- Let's follow an example step by step from "the rails way" to a proper way to understand it better.
58
+ Shirinji is unobtrusive. Basically, any of your objects can be used
59
+ outside of its context.
26
60
 
27
61
  ```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
62
+ bar_service = BarService.new
63
+ foo_service = FooService.new(bar_service: bar_service)
64
+ # => <#FooService @bar_service=<#BarService>>
65
+
66
+ # tests
67
+
68
+ RSpec.describe FooService do
69
+ let(:bar_service) { double(call: nil) }
70
+ let(:service) { described_class.new(bar_service: bar_service) }
36
71
 
37
- def send_confirmation_email
38
- UserMailer.confirm_email(user).deliver
72
+ describe '.call' do
73
+ # ...
39
74
  end
40
75
  end
41
76
  ```
42
77
 
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 ...)
78
+ ## Constructor arguments
79
+
80
+ Shirinji relies on constructor to inject dependencies. It's considering that
81
+ objects that receive dependencies should be immutables and those dependencies
82
+ should not change during your program lifecycle.
83
+
84
+ Shirinji doesn't accept anything else than named parameters. This way,
85
+ arguments order doesn't matter and it makes everybody's life easier.
86
+
87
+ ## Name resolution
48
88
 
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.
89
+ By default, when you try to resolve a bean, Shirinji will look for a bean named
90
+ accordingly for each constructor parameter.
91
+
92
+ It's possible to locally override this behaviour though by using `attr` macro.
51
93
 
52
94
  ```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
95
+ class FooService
96
+ attr_reader :bar_service
97
+
98
+ def initialize(my_service:)
99
+ @bar_service = my_service
59
100
  end
60
101
  end
61
102
 
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) }
103
+ map = Shirinji::Map.new do
104
+ bean(:foo_service, klass: 'FooService') do
105
+ attr :my_service, ref: :bar_service
106
+ end
70
107
 
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
108
+ bean(:bar_service, klass: 'BarService')
101
109
  end
110
+
111
+ resolver = Shirinji::Resolver.new(map)
112
+
113
+ resolver.resolve(:foo_service)
114
+ # => <#FooService @bar_service=<#BarService>>
102
115
  ```
103
116
 
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.
117
+ ## Caching and singletons
106
118
 
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)
119
+ Shirinji provides a caching mecanism to help you improve memory consumption.
120
+ This cache is safe as long as your beans remains immutable (they should always
121
+ be).
111
122
 
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.
123
+ The consequence is that any cached instance is actually a singleton. Singleton
124
+ is no more a property of your class but of it's environment, improving the
125
+ reusability of your code.
116
126
 
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.
127
+ Singleton is the default access mode for a bean.
119
128
 
120
129
  ```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
+ map = Shirinji::Map.new do
131
+ bean(:bar_service, klass: 'BarService', access: :instance)
132
+ bean(:foo_service, klass: 'FooService', access: :singleton)
133
+ # same as bean(:foo_service, klass: 'FooService')
130
134
  end
135
+
136
+ resolver = Shirinji::Resolver.new(map)
137
+
138
+ resolver.resolve(:foo_service).object_id #=> 1
139
+ resolver.resolve(:foo_service).object_id #=> 1
140
+
141
+ resolver.resolve(:bar_service).object_id #=> 2
142
+ resolver.resolve(:bar_service).object_id #=> 3
131
143
  ```
132
144
 
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
145
+ Cache can be reset with the simple command `resolver.reset_cache`, which can be
146
+ useful when using a development console like rails console ([shirinji-rails](
147
+ https://github.com/fdutey/shirinji-rails) is attaching cache reset to `reload!`
148
+ command).
149
+
150
+ ## Other type of beans
151
+
152
+ Dependencies injection doesn't apply only to classes. You can actually inject
153
+ anything and therefore, Shirinji allows you to declare anything as a dependency.
154
+ To achieve that, use the key `value` instead of `class`.
136
155
 
137
156
  ```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)
157
+ module MyApp
158
+ def self.config
159
+ @config
145
160
  end
146
-
147
- private
148
161
 
149
- def publish_user_statistics_service
150
- PublishUserStatisticsService.new
151
- end
152
-
153
- def send_user_email_confirmation_service
154
- SendUserEmailConfirmationService.new
162
+ def self.load!
163
+ @config = OpenStruct.new
155
164
  end
156
165
  end
157
166
 
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 }
167
+ class FooService
168
+ attr_reader :config
165
169
 
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)
170
+ def initialize(config:)
171
+ @config = config
169
172
  end
170
-
171
- # ...
172
173
  end
173
- ```
174
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.
175
+ MyApp.load!
180
176
 
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.
177
+ map = Shirinji::Map.new do
178
+ bean(:config, value: Proc.new { MyApp.config })
179
+
180
+ bean(:foo_service, klass: 'FooService')
181
+ end
183
182
 
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
183
+ resolver = Shirinji::Resolver.new(map)
196
184
 
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
185
+ resolver.resolve(:foo_service)
186
+ #=> <#FooService @config=<#OpenStruct ...> ...>
205
187
  ```
206
188
 
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.
189
+ A value can be anything. `Proc` will be lazily evaluated. It also obeys the
190
+ cache mechanism described before.
209
191
 
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.
192
+ ## Skip construction mechanism
212
193
 
213
- ## Usage
194
+ In some cases, you need a dependency to be injected as a class and not an
195
+ instance. In such case, you could use value beans, returning the class itself,
196
+ but you would lose the benefit of scopes (see below).
197
+ Instead, Shirinji provides a parameter to skip the object construction.
198
+
199
+ A real life example is a Job where `deliver_now` and `deliver_later` are
200
+ class methods.
214
201
 
215
202
  ```ruby
216
203
  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")
204
+ bean(:foo_job, klass: 'FooJob', construct: false)
220
205
  end
221
206
 
222
207
  resolver = Shirinji::Resolver.new(map)
223
208
 
224
- resolver.resolve(:sign_up_user_service)
225
- #=> <#SignUpUserService @publish_user_statistics_service=<#PublishUserStatisticsService ...> ...>
209
+ resolver.resolve(:foo_job) #=> FooJob
226
210
  ```
227
211
 
228
- In this example, because `SingUpUserService` constructor parameters match beans with the same name,
229
- Shirinji will automatically resolve them.
212
+ ## Scopes
230
213
 
231
- In a case where a parameter name match no bean, it has to be mapped explicitly.
214
+ Building complex objects mapping leads to lot of repetition. That's why Shirinji
215
+ also provides a scope mechanism to help you dry your code.
232
216
 
233
217
  ```ruby
234
218
  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
219
+ scope module: :Services, suffix: :service, klass_suffix: :Service do
220
+ bean(:foo, klass: 'Foo')
221
+ # same as bean(:foo_service, klass: 'Services::FooService')
222
+
223
+ scope module: :User, prefix: :user do
224
+ bean(:bar, klass: 'Bar')
225
+ # same as bean(:user_bar_service, klass: 'Services::User::BarService')
226
+ end
237
227
  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
228
  end
229
+ ```
243
230
 
244
- resolver = Shirinji::Resolver.new(map)
231
+ Scopes also come with an `auto_klass` attribute to save even more time for
232
+ common cases
245
233
 
246
- resolver.resolve(:sign_up_user_service)
247
- #=> <#SignUpUserService @publish_user_statistics_service=<#PublishUserStatisticsService ...> ...>
234
+ ```ruby
235
+ map = Shirinji::Map.new do
236
+ scope module: :Services,
237
+ suffix: :service,
238
+ klass_suffix: :Service,
239
+ auto_klass: true do
240
+ bean(:foo)
241
+ # same as bean(:foo_service, klass: 'Services::FooService')
242
+ end
243
+ end
248
244
  ```
249
245
 
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.
246
+ Scopes also provides an `auto_prefix` option
253
247
 
254
- Singleton is the default access mode for a bean.
248
+ ```ruby
249
+ map = Shirinji::Map.new do
250
+ scope module: :Services,
251
+ suffix: :service,
252
+ klass_suffix: :Service,
253
+ auto_klass: true do
254
+
255
+ # Do not use auto prefix on root scope or every bean will be prefixed
256
+ # with `services_`
257
+ scope auto_prefix: true do
258
+ bean(:foo)
259
+ # same as bean(:foo_service, klass: 'Services::FooService')
260
+
261
+ scope module: :User do
262
+ # same as scope module: :User, prefix: :user
263
+
264
+ bean(:bar)
265
+ # same as bean(:user_bar_service, klass: 'Services::User::BarService')
266
+ end
267
+ end
268
+ end
269
+ end
270
+ ```
271
+
272
+ Finally, for mailers / jobs ..., Scopes allow you to specify a global value
273
+ for `construct`
255
274
 
256
275
  ```ruby
257
276
  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
277
+ scope module: :Jobs,
278
+ suffix: :job,
279
+ klass_suffix: :Job,
280
+ auto_klass: true,
281
+ construct: false do
282
+ bean(:foo)
283
+ # bean(:foo_job, klass: 'Jobs::FooJob', construct: false)
284
+ end
260
285
  end
286
+ ```
261
287
 
262
- resolver = Shirinji::Resolver.new(map)
288
+ Scopes do not carry property `access`
263
289
 
264
- resolver.resolve(:foo).object_id #=> 1
265
- resolver.resolve(:foo).object_id #=> 1
290
+ ## Code splitting
266
291
 
267
- resolver.resolve(:bar).object_id #=> 2
268
- resolver.resolve(:bar).object_id #=> 3
269
- ```
292
+ When a project grows, dependencies grows too. Keeping them into one single file
293
+ leads to headaches. One possible solution to keep everything under control is
294
+ to split your dependencies into many files.
270
295
 
271
- You can also create beans that contain single values. It will help you to avoid referencing global
272
- variables in your code.
296
+ To include a "sub-map" into another one, you can use `include_map` method.
273
297
 
274
298
  ```ruby
275
- map = Shirinji::Map.new do
276
- bean(:config, value: Proc.new { Application.config })
277
- bean(:foo, klass: 'Foo')
299
+ # dependencies/services.rb
300
+ Shirinji::Map.new do
301
+ bean(:foo_service, klass: 'FooService')
278
302
  end
279
303
 
280
- resolver = Shirinji::Resolver.new(map)
304
+ # dependencies/queries.rb
305
+ Shirinji::Map.new do
306
+ bean(:foo_query, klass: 'FooQuery')
307
+ end
281
308
 
282
- class Foo
283
- attr_reader :config
309
+ # dependencies.rb
310
+
311
+ root = Pathname.new(File.expand_path('../dependencies', __FILE__))
312
+
313
+ Shirinji::Map.new do
314
+ bean(:config, value: -> { MyApp.config })
284
315
 
285
- def initialize(config:)
286
- @config = config
287
- end
316
+ # paths must be absolute
317
+ include_map(root.join('queries.rb'))
318
+ include_map(root.join('services.rb'))
288
319
  end
289
-
290
- resolver.resolve(:foo)
291
- #=> <#Foo @config=<#OpenStruct ...> ...>
292
320
  ```
293
321
 
294
- Values can be anything. A `Proc` will be lazily evaluated. They also obey the singleton / instance
295
- strategy.
296
-
297
322
  ## Notes
298
323
 
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.
324
+ - It is absolutely mandatory for your beans to be stateless to use the singleton
325
+ mode. If they're not, you will probably run into trouble as your objects
326
+ behavior will depend on their history, leading to unpredictable effects.
327
+ - Shirinji only works with named arguments. It will raise `ArgumentError` if you
328
+ try to use it with "standard" method arguments.
329
+
330
+ ## TODOS
331
+
332
+ - solve absolute paths problems for `include_map` (`instance_eval` is a problem)
304
333
 
305
334
  ## Contributing
306
335
 
307
- Bug reports and pull requests are welcome on GitHub at https://github.com/fdutey/shirinji.
336
+ Bug reports and pull requests are welcome on GitHub at
337
+ https://github.com/fdutey/shirinji.
data/lib/shirinji.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shirinji
4
- # Your code goes here...
5
4
  end
6
5
 
7
6
  require 'shirinji/version'
@@ -10,3 +9,4 @@ require 'shirinji/bean'
10
9
  require 'shirinji/map'
11
10
  require 'shirinji/resolver'
12
11
  require 'shirinji/scope'
12
+ require 'shirinji/utils'
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Shirinji
4
4
  class Attribute
5
- attr_reader :name, :reference
5
+ attr_reader :name, :reference, :value
6
6
 
7
- def initialize(name, reference)
7
+ def initialize(name, reference = nil, value = nil)
8
8
  @name = name
9
9
  @reference = reference
10
+ @value = value
10
11
  end
11
12
  end
12
13
  end
data/lib/shirinji/bean.rb CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  module Shirinji
4
4
  class Bean
5
- attr_reader :name, :class_name, :value, :access, :attributes
5
+ attr_reader :name, :class_name, :value, :access, :attributes, :construct
6
6
 
7
- def initialize(name, class_name: nil, value: nil, access:, &block)
7
+ # rubocop:disable Metrics/ParameterLists
8
+ def initialize(
9
+ name, class_name: nil, value: nil, access:, construct: true, &block
10
+ )
8
11
  check_params!(class_name, value)
9
12
 
10
13
  @name = name
@@ -12,12 +15,14 @@ module Shirinji
12
15
  @value = value
13
16
  @access = access
14
17
  @attributes = {}
18
+ @construct = construct
15
19
 
16
20
  instance_eval(&block) if block
17
21
  end
22
+ # rubocop:enable Metrics/ParameterLists
18
23
 
19
- def attr(name, ref:)
20
- attributes[name] = Attribute.new(name, ref)
24
+ def attr(name, ref: nil, value: nil)
25
+ attributes[name] = Attribute.new(name, ref, value)
21
26
  end
22
27
 
23
28
  private
data/lib/shirinji/map.rb CHANGED
@@ -4,10 +4,34 @@ module Shirinji
4
4
  class Map
5
5
  attr_reader :beans
6
6
 
7
+ # Loads a map at a given location
8
+ #
9
+ # @param location [string] path to the map to load
10
+ def self.load(location)
11
+ eval(File.read(location))
12
+ end
13
+
7
14
  def initialize(&block)
8
15
  @beans = {}
9
16
 
10
- instance_eval(&block)
17
+ instance_eval(&block) if block
18
+ end
19
+
20
+ # Merges another map at a given location
21
+ #
22
+ # @param location [string] the file to include - must be an absolute path
23
+ def include_map(location)
24
+ merge(self.class.load(location))
25
+ end
26
+
27
+ # Merges a map into another one
28
+ #
29
+ # @param map [Shirinji::Map] the map to merge into this one
30
+ # @raise [ArgumentError] if both map contains a bean with the same bean
31
+ def merge(map)
32
+ map.beans.keys.each { |name| raise_if_name_already_taken!(name) }
33
+
34
+ beans.merge!(map.beans)
11
35
  end
12
36
 
13
37
  # Returns a bean based on its name
@@ -26,6 +50,7 @@ module Shirinji
26
50
  def get(name)
27
51
  bean = beans[name.to_sym]
28
52
  raise ArgumentError, "Unknown bean #{name}" unless bean
53
+
29
54
  bean
30
55
  end
31
56
 
@@ -47,20 +72,20 @@ module Shirinji
47
72
  #
48
73
  # @param name [Symbol] the name you want to register your bean
49
74
  # @option [String] :klass the classname the bean is registering
50
- # @option [*] :value the object registered by the bean
75
+ # @option [Object] :value the object registered by the bean
76
+ # @option [Boolean] :construct whether the bean should be constructed or not
51
77
  # @option [Symbol] :access either :singleton or :instance.
52
78
  # @yield additional method to construct our bean
53
79
  # @raise [ArgumentError] if trying to register a bean that already exist
54
- def bean(name, klass: nil, value: nil, access: :singleton, &block)
80
+ def bean(name, klass: nil, access: :singleton, **others, &block)
55
81
  name = name.to_sym
56
82
 
57
83
  raise_if_name_already_taken!(name)
58
84
 
59
- options = {
85
+ options = others.merge(
60
86
  access: access,
61
- class_name: klass ? klass.freeze : nil,
62
- value: value
63
- }
87
+ class_name: klass&.freeze
88
+ )
64
89
 
65
90
  beans[name] = Bean.new(name, **options, &block)
66
91
  end
@@ -113,6 +138,9 @@ module Shirinji
113
138
  # @option options [Symbol] :prefix prepend prefix to bean name
114
139
  # @option options [Symbol] :suffix append suffix to bean name
115
140
  # @option options [Symbol] :klass_suffix append suffix to class name
141
+ # @option options [Boolean] :auto_klass generates klass from name
142
+ # @option options [Boolean] :auto_prefix generates prefix from module
143
+ # @option options [Boolean] :construct applies `construct` on every bean
116
144
  # @yield a standard map
117
145
  def scope(**options, &block)
118
146
  Scope.new(self, **options, &block)
@@ -122,6 +150,7 @@ module Shirinji
122
150
 
123
151
  def raise_if_name_already_taken!(name)
124
152
  return unless beans[name]
153
+
125
154
  msg = "A bean already exists with the following name: #{name}"
126
155
  raise ArgumentError, msg
127
156
  end
@@ -24,6 +24,11 @@ module Shirinji
24
24
  end
25
25
  end
26
26
 
27
+ def reload(map)
28
+ @map = map
29
+ reset_cache
30
+ end
31
+
27
32
  def reset_cache
28
33
  @singletons = {}
29
34
  end
@@ -40,12 +45,13 @@ module Shirinji
40
45
 
41
46
  def resolve_class_bean(bean)
42
47
  klass, params = resolve_class(bean)
48
+ return klass unless bean.construct
43
49
  return klass.new if params.empty?
44
50
 
45
51
  check_params!(params)
46
52
 
47
53
  args = params.each_with_object({}) do |(_type, arg), memo|
48
- memo[arg] = resolve(resolve_attribute(bean, arg))
54
+ memo[arg] = resolve_attribute(bean, arg)
49
55
  end
50
56
 
51
57
  klass.new(**args)
@@ -59,12 +65,16 @@ module Shirinji
59
65
  end
60
66
 
61
67
  def resolve_attribute(bean, arg)
62
- (attr = bean.attributes[arg]) ? attr.reference : arg
68
+ return resolve(arg) unless (attr = bean.attributes[arg])
69
+ return attr.value if attr.value
70
+
71
+ resolve(attr.reference)
63
72
  end
64
73
 
65
74
  def check_params!(params)
66
75
  params.each do |pair|
67
76
  next if ARG_TYPES.include?(pair.first)
77
+
68
78
  raise ArgumentError, 'Only key arguments are allowed'
69
79
  end
70
80
  end
@@ -2,42 +2,86 @@
2
2
 
3
3
  module Shirinji
4
4
  class Scope
5
- VALID_OPTIONS = %i[module prefix suffix klass_suffix].freeze
5
+ VALID_OPTIONS = %i[
6
+ module prefix suffix klass_suffix auto_klass auto_prefix construct
7
+ ].freeze
6
8
 
7
- attr_reader :parent, :mod, :prefix, :suffix, :klass_suffix
9
+ attr_reader :parent, :mod, :prefix, :suffix, :klass_suffix, :auto_klass,
10
+ :construct, :auto_prefix
8
11
 
9
12
  def initialize(parent, **options, &block)
10
13
  validate_options(options)
11
14
 
12
15
  @parent = parent
13
16
  @mod = options[:module]
14
- @prefix = options[:prefix]
15
17
  @suffix = options[:suffix]
16
18
  @klass_suffix = options[:klass_suffix]
19
+ @auto_klass = options[:auto_klass]
20
+ @auto_prefix = options[:auto_prefix]
21
+ @prefix = generate_prefix(options[:prefix])
22
+ @construct = options.fetch(:construct, true)
17
23
 
18
24
  instance_eval(&block) if block
19
25
  end
20
26
 
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
- }
27
+ def bean(name, klass: nil, **options, &block)
28
+ default_opts = compact({ construct: construct })
29
+
30
+ klass = generate_klass(name, klass) unless options[:value]
31
+ options = compact(default_opts.merge(options).merge(klass: klass))
32
+ scoped_name = generate_scope(name)
28
33
 
29
- parent.bean([prefix, name, suffix].compact.join('_'), **options, &block)
34
+ parent.bean(scoped_name, **options, &block)
30
35
  end
31
36
 
32
37
  def scope(**options, &block)
33
- Scope.new(self, **options, &block)
38
+ opts = {
39
+ auto_klass: auto_klass,
40
+ auto_prefix: auto_prefix,
41
+ construct: construct
42
+ }.merge(options)
43
+
44
+ Scope.new(self, **opts, &block)
34
45
  end
35
46
 
36
47
  private
37
48
 
49
+ def compact(h)
50
+ h.reject { |_,v| v.nil? }
51
+ end
52
+
53
+ def generate_scope(name)
54
+ [prefix, name, suffix].compact.join('_')
55
+ end
56
+
57
+ def generate_klass(name, klass)
58
+ return if !klass && !auto_klass
59
+
60
+ klass ||= klassify(name)
61
+ chunks = [mod, "#{klass}#{klass_suffix}"].compact
62
+
63
+ chunks.join('::')
64
+ end
65
+
66
+ def generate_prefix(prefix)
67
+ return prefix if prefix
68
+ return nil unless auto_prefix
69
+
70
+ mod && underscore(mod.to_s).to_sym
71
+ end
72
+
73
+ def klassify(name)
74
+ Shirinji::Utils::String.camelcase(name)
75
+ end
76
+
77
+ def underscore(name)
78
+ Shirinji::Utils::String.snakecase(name)
79
+ end
80
+
38
81
  def validate_options(args)
39
82
  args.each_key do |k|
40
83
  next if Shirinji::Scope::VALID_OPTIONS.include?(k)
84
+
41
85
  raise ArgumentError, "Unknown key #{k}"
42
86
  end
43
87
  end
@@ -0,0 +1,6 @@
1
+ module Shirinji
2
+ module Utils
3
+ end
4
+ end
5
+
6
+ require_relative 'utils/string'
@@ -0,0 +1,24 @@
1
+ module Shirinji
2
+ module Utils
3
+ module String
4
+ CAMEL =
5
+ /[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/
6
+
7
+ module_function
8
+
9
+ def camelcase(str)
10
+ chunks = str.to_s.split('_').map do |w|
11
+ w = w.downcase
12
+ w[0] = w[0].upcase
13
+ w
14
+ end
15
+
16
+ chunks.join
17
+ end
18
+
19
+ def snakecase(str)
20
+ str.gsub(CAMEL) { |s| s.split('').join('_') }.downcase
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shirinji
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.7'
5
5
  end
data/shirinji.gemspec CHANGED
@@ -1,7 +1,6 @@
1
-
2
1
  # frozen_string_literal: true
3
2
 
4
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
5
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
5
  require 'shirinji/version'
7
6
 
@@ -22,7 +21,6 @@ Gem::Specification.new do |spec|
22
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
22
  spec.require_paths = ['lib']
24
23
 
25
- spec.add_development_dependency 'bundler', '~> 1.16'
26
- spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'rake', '>= 12.3.3'
27
25
  spec.add_development_dependency 'rspec', '~> 3.0'
28
26
  end
metadata CHANGED
@@ -1,43 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shirinji
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Dutey
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-03-12 00:00:00.000000000 Z
11
+ date: 2021-04-15 00:00:00.000000000 Z
12
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
13
  - !ruby/object:Gem::Dependency
28
14
  name: rake
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
- - - "~>"
17
+ - - ">="
32
18
  - !ruby/object:Gem::Version
33
- version: '10.0'
19
+ version: 12.3.3
34
20
  type: :development
35
21
  prerelease: false
36
22
  version_requirements: !ruby/object:Gem::Requirement
37
23
  requirements:
38
- - - "~>"
24
+ - - ">="
39
25
  - !ruby/object:Gem::Version
40
- version: '10.0'
26
+ version: 12.3.3
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: rspec
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -59,12 +45,14 @@ executables: []
59
45
  extensions: []
60
46
  extra_rdoc_files: []
61
47
  files:
48
+ - ".codeclimate.yml"
62
49
  - ".gitignore"
63
50
  - ".rspec"
64
51
  - ".rubocop.yml"
65
52
  - ".ruby-gemset"
66
53
  - ".ruby-version"
67
54
  - ".travis.yml"
55
+ - CODE_OF_CONDUCT.md
68
56
  - Gemfile
69
57
  - Gemfile.lock
70
58
  - README.md
@@ -77,12 +65,14 @@ files:
77
65
  - lib/shirinji/map.rb
78
66
  - lib/shirinji/resolver.rb
79
67
  - lib/shirinji/scope.rb
68
+ - lib/shirinji/utils.rb
69
+ - lib/shirinji/utils/string.rb
80
70
  - lib/shirinji/version.rb
81
71
  - shirinji.gemspec
82
72
  homepage: https://github.com/fdutey/shirinji
83
73
  licenses: []
84
74
  metadata: {}
85
- post_install_message:
75
+ post_install_message:
86
76
  rdoc_options: []
87
77
  require_paths:
88
78
  - lib
@@ -97,9 +87,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
87
  - !ruby/object:Gem::Version
98
88
  version: '0'
99
89
  requirements: []
100
- rubyforge_project:
101
- rubygems_version: 2.6.14
102
- signing_key:
90
+ rubygems_version: 3.2.15
91
+ signing_key:
103
92
  specification_version: 4
104
93
  summary: Dependencies injection made easy
105
94
  test_files: []