shirinji 0.0.2 → 0.0.7

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 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: []