shirinji 0.0.3 → 0.0.4

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
2
  SHA1:
3
- metadata.gz: f263ef5a2e01c4aa04a72dccf8c114b250559983
4
- data.tar.gz: 70517a5f3fe891b5bf4061156af846c657a0b8d9
3
+ metadata.gz: 77fda6000796dd02133fae4778b4501bc570990f
4
+ data.tar.gz: 46c1303d621e5fb851a4cc01514a4451df06535f
5
5
  SHA512:
6
- metadata.gz: e64293607acfd7fa5fa90a853a799521c2d9eb1c29b7c4dfae4a5ed8d293be40507d370b535f6133b1ef22aacafb9f1e8709dc1cf0fd8ae3baa646fd5e697d8f
7
- data.tar.gz: e14c3e48100be30c67057578a303353e65b9e0d01acd0435358a5fe796f8bdb0a119c49d6b26452bb84ce6a62675affa4b9d63552b8db9978bc1b7b7cec40c2d
6
+ metadata.gz: 12e6a82f1e8b7fea438a05bdd7020cabc0197ed2e1ec6398bd0d19b81031a98bb29eeaeeba85b9064d3b11cd213156b9123ff813961e4b374f3a32bbc53d75b3
7
+ data.tar.gz: 35f01c0f061af476ccab1704656c4187fa5284c025aabd332e89dd7b8b511e5a19f9ab2c73380649e6fc715d71965e6e6066f5c01d632f7163b35a6d8fa53133
data/.codeclimate.yml ADDED
@@ -0,0 +1,3 @@
1
+ plugins:
2
+ rubocop:
3
+ enabled: true
data/.travis.yml CHANGED
@@ -1,5 +1,14 @@
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.5.0
10
+ - 2.5.1
11
+ - 2.5.2
12
+ - 2.5.3
13
+ - 2.6.0
14
+ before_install: gem install bundler -v 1.16.6
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shirinji (0.0.3)
4
+ shirinji (0.0.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -26,10 +26,9 @@ PLATFORMS
26
26
  ruby
27
27
 
28
28
  DEPENDENCIES
29
- bundler (~> 1.16)
30
29
  rake (~> 10.0)
31
30
  rspec (~> 3.0)
32
31
  shirinji!
33
32
 
34
33
  BUNDLED WITH
35
- 1.16.1
34
+ 2.0.2
data/README.md CHANGED
@@ -1,333 +1,335 @@
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)
4
12
 
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.
13
+ Dependencies Injection made clean and easy for Ruby.
9
14
 
10
- Yet, it's heavily used in javascript world and fit perfectly with prototyped language.
15
+ ## Supported ruby versions
11
16
 
12
- ```javascript
13
- function updateUI(evt) { /* ... */ }
14
-
15
- $.ajax('/action', { onSuccess: updateUI, ... })
16
- ```
17
+ - 2.4.x
18
+ - 2.5.x
19
+ - 2.6.x
17
20
 
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
+ ## Principles
21
22
 
22
- Dependencies injection is nothing more than the exact same principle but applied to objects instead
23
- of functions.
23
+ Remove hard dependencies between your objects and delegate object tree building
24
+ to an unobtrusive framework with cool convention over configuration.
24
25
 
25
- Let's follow an example step by step from "the rails way" to a proper way to understand it better.
26
+ Shirinji relies on a mapping of beans and a resolver. When you resolve a bean,
27
+ it will return (by default) an instance of the class associated to the bean,
28
+ with all the bean dependencies resolved.
26
29
 
27
30
  ```ruby
28
- class User < ActiveRecord::Base
29
- after_create :publish_statistics, :send_confirmation_email
30
-
31
- private
31
+ class FooService
32
+ attr_reader :bar_service
32
33
 
33
- def publish_statistics
34
- StatisticsGateway.publish_event(:new_user, user.id)
34
+ def initialize(bar_service:)
35
+ @bar_service = bar_service
35
36
  end
36
37
 
37
- def send_confirmation_email
38
- UserMailer.confirm_email(user).deliver
38
+ def call(obj)
39
+ obj.foo = 123
40
+
41
+ bar_service.call(obj)
39
42
  end
40
43
  end
41
- ```
42
44
 
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 ...)
45
+ map = Shirinji::Map.new do
46
+ bean(:foo_service, klass: 'FooService')
47
+ bean(:bar_service, klass: 'BarService')
48
+ end
48
49
 
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.
50
+ resolver = Shirinji::Resolver.new(map)
51
+
52
+ resolver.resolve(:foo_service)
53
+ # => <#FooService @bar_service=<#BarService>>
54
+ ```
55
+
56
+ Shirinji is unobtrusive. Basically, any of your objects can be used
57
+ outside of its context.
51
58
 
52
59
  ```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
60
+ bar_service = BarService.new
61
+ foo_service = FooService.new(bar_service: bar_service)
62
+ # => <#FooService @bar_service=<#BarService>>
61
63
 
62
- ## test
64
+ # tests
63
65
 
64
- RSpec.describe SignUpUserService do
65
- let(:service) { described_class.new }
66
+ RSpec.describe FooService do
67
+ let(:bar_service) { double(call: nil) }
68
+ let(:service) { described_class.new(bar_service: bar_service) }
66
69
 
67
70
  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
71
+ # ...
72
+ end
101
73
  end
102
74
  ```
103
75
 
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.
76
+ ## Constructor arguments
106
77
 
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)
78
+ Shirinji relies on constructor to inject dependencies. It's considering that
79
+ objects that receive dependencies should be immutables and those dependencies
80
+ should not change during your program lifecycle.
111
81
 
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.
82
+ Shirinji doesn't accept anything else than named parameters. This way,
83
+ arguments order doesn't matter and it makes everybody's life easier.
116
84
 
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.
85
+ ## Name resolution
119
86
 
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
- ```
87
+ By default, when you try to resolve a bean, Shirinji will look for a bean named
88
+ accordingly for each constructor parameter.
132
89
 
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
90
+ It's possible to locally override this behaviour though by using `attr` macro.
136
91
 
137
92
  ```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
93
+ class FooService
94
+ attr_reader :bar_service
148
95
 
149
- def publish_user_statistics_service
150
- PublishUserStatisticsService.new
151
- end
152
-
153
- def send_user_email_confirmation_service
154
- SendUserEmailConfirmationService.new
96
+ def initialize(my_service:)
97
+ @bar_service = my_service
155
98
  end
156
99
  end
157
100
 
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)
101
+ map = Shirinji::Map.new do
102
+ bean(:foo_service, klass: 'FooService') do
103
+ attr :my_service, ref: :bar_service
169
104
  end
170
105
 
171
- # ...
106
+ bean(:bar_service, klass: 'BarService')
172
107
  end
108
+
109
+ resolver = Shirinji::Resolver.new(map)
110
+
111
+ resolver.resolve(:foo_service)
112
+ # => <#FooService @bar_service=<#BarService>>
173
113
  ```
174
114
 
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.
115
+ ## Caching and singletons
180
116
 
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.
117
+ Shirinji provides a caching mecanism to help you improve memory consumption.
118
+ This cache is safe as long as your beans remains immutable (they should always
119
+ be).
183
120
 
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
121
+ The consequence is that any cached instance is actually a singleton. Singleton
122
+ is no more a property of your class but of it's environment, improving the
123
+ reusability of your code.
196
124
 
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
125
+ Singleton is the default access mode for a bean.
126
+
127
+ ```ruby
128
+ map = Shirinji::Map.new do
129
+ bean(:bar_service, klass: 'BarService', access: :instance)
130
+ bean(:foo_service, klass: 'FooService', access: :singleton)
131
+ # same as bean(:foo_service, klass: 'FooService')
204
132
  end
133
+
134
+ resolver = Shirinji::Resolver.new(map)
135
+
136
+ resolver.resolve(:foo).object_id #=> 1
137
+ resolver.resolve(:foo).object_id #=> 1
138
+
139
+ resolver.resolve(:bar).object_id #=> 2
140
+ resolver.resolve(:bar).object_id #=> 3
205
141
  ```
206
142
 
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.
143
+ Cache can be reset with the simple command `resolver.reset_cache`, which can be
144
+ useful when using a development console like rails console ([shirinji-rails](
145
+ https://github.com/fdutey/shirinji-rails) is attaching cache reset to `reload!`
146
+ command).
209
147
 
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.
148
+ ## Other type of beans
212
149
 
213
- ## Usage
150
+ Dependencies injection doesn't apply only to classes. You can actually inject
151
+ anything and therefore, Shirinji allows you to declare anything as a dependency.
152
+ To achieve that, use the key `value` instead of `class`.
214
153
 
215
154
  ```ruby
155
+ module MyApp
156
+ def self.config
157
+ @config
158
+ end
159
+
160
+ def self.load!
161
+ @config = OpenStruct.new
162
+ end
163
+ end
164
+
165
+ class FooService
166
+ attr_reader :config
167
+
168
+ def initialize(config:)
169
+ @config = config
170
+ end
171
+ end
172
+
173
+ MyApp.load!
174
+
216
175
  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")
176
+ bean(:config, value: Proc.new { MyApp.config })
177
+
178
+ bean(:foo_service, klass: 'FooService')
220
179
  end
221
180
 
222
181
  resolver = Shirinji::Resolver.new(map)
223
182
 
224
- resolver.resolve(:sign_up_user_service)
225
- #=> <#SignUpUserService @publish_user_statistics_service=<#PublishUserStatisticsService ...> ...>
183
+ resolver.resolve(:foo_service)
184
+ #=> <#FooService @config=<#OpenStruct ...> ...>
226
185
  ```
227
186
 
228
- In this example, because `SingUpUserService` constructor parameters match beans with the same name,
229
- Shirinji will automatically resolve them.
187
+ A value can be anything. `Proc` will be lazily evaluated. It also obeys the
188
+ cache mechanism described before.
189
+
190
+ ## Skip construction mechanism
230
191
 
231
- In a case where a parameter name match no bean, it has to be mapped explicitly.
192
+ In some cases, you need a dependency to be injected as a class and not an
193
+ instance. In such case, you could use value beans, returning the class itself,
194
+ but you would lose the benefit of scopes (see below).
195
+ Instead, Shirinji provides a parameter to skip the object construction.
196
+
197
+ A real life example is a Job where `deliver_now` and `deliver_later` are
198
+ class methods.
232
199
 
233
200
  ```ruby
234
201
  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")
202
+ bean(:foo_job, klass: 'FooJob', construct: false)
242
203
  end
243
204
 
244
205
  resolver = Shirinji::Resolver.new(map)
245
206
 
246
- resolver.resolve(:sign_up_user_service)
247
- #=> <#SignUpUserService @publish_user_statistics_service=<#PublishUserStatisticsService ...> ...>
207
+ resolver.resolve(:foo_job) #=> FooJob
248
208
  ```
249
209
 
250
- Shirinji provides scopes to help you organize your dependencies
210
+ ## Scopes
211
+
212
+ Building complex objects mapping leads to lot of repetition. That's why Shirinji
213
+ also provides a scope mechanism to help you dry your code.
251
214
 
252
215
  ```ruby
253
216
  map = Shirinji::Map.new do
254
217
  scope module: :Services, suffix: :service, klass_suffix: :Service do
218
+ bean(:foo, klass: 'Foo')
219
+ # same as bean(:foo_service, klass: 'Services::FooService')
220
+
255
221
  scope module: :User, prefix: :user do
256
- bean(:signup, klass: 'Signup')
257
- end
222
+ bean(:bar, klass: 'Bar')
223
+ # same as bean(:user_bar_service, klass: 'Services::User::BarService')
224
+ end
258
225
  end
259
-
260
- # is the same as
261
- bean(:user_signup_service, klass: 'Services::User::SignupService')
262
226
  end
263
227
  ```
264
228
 
265
- If you need a dependency to return a class instead of an instance, you can disable
266
- the bean construction
229
+ Scopes also come with an `auto_klass` attribute to save even more time for
230
+ common cases
267
231
 
268
232
  ```ruby
269
233
  map = Shirinji::Map.new do
270
- bean(:foo, klass: 'Foo', construct: false)
234
+ scope module: :Services,
235
+ suffix: :service,
236
+ klass_suffix: :Service,
237
+ auto_klass: true do
238
+ bean(:foo)
239
+ # same as bean(:foo_service, klass: 'Services::FooService')
240
+ end
271
241
  end
272
-
273
- resolver.resolve(:foo) #=> Foo
274
242
  ```
275
243
 
276
- Shirinji also provide a caching mecanism to achieve singleton pattern without having to implement
277
- the pattern in your classes. It means the same class can be used as a singleton AND a regular class
278
- at the same time without any code change.
244
+ Scopes also provides an `auto_prefix` option
279
245
 
280
- Singleton is the default access mode for a bean.
246
+ ```ruby
247
+ map = Shirinji::Map.new do
248
+ scope module: :Services,
249
+ suffix: :service,
250
+ klass_suffix: :Service,
251
+ auto_klass: true do
252
+
253
+ # Do not use auto prefix on root scope or every bean will be prefixed
254
+ # with `services_`
255
+ scope auto_prefix: true do
256
+ bean(:foo)
257
+ # same as bean(:foo_service, klass: 'Services::FooService')
258
+
259
+ scope module: :User do
260
+ # same as scope module: :User, prefix: :user
261
+
262
+ bean(:bar)
263
+ # same as bean(:user_bar_service, klass: 'Services::User::BarService')
264
+ end
265
+ end
266
+ end
267
+ end
268
+ ```
269
+
270
+ Finally, for mailers / jobs ..., Scopes allow you to specify a global value
271
+ for `construct`
281
272
 
282
273
  ```ruby
283
274
  map = Shirinji::Map.new do
284
- bean(:foo, klass: 'Foo', access: :singleton) # foo is singleton
285
- bean(:bar, klass: 'Bar', access: :instance) # bar is not
275
+ scope module: :Jobs,
276
+ suffix: :job,
277
+ klass_suffix: :Job,
278
+ auto_klass: true,
279
+ construct: false do
280
+ bean(:foo)
281
+ # bean(:foo_job, klass: 'Jobs::FooJob', construct: false)
282
+ end
286
283
  end
284
+ ```
287
285
 
288
- resolver = Shirinji::Resolver.new(map)
286
+ Scopes do not carry property `access`
289
287
 
290
- resolver.resolve(:foo).object_id #=> 1
291
- resolver.resolve(:foo).object_id #=> 1
288
+ ## Code splitting
292
289
 
293
- resolver.resolve(:bar).object_id #=> 2
294
- resolver.resolve(:bar).object_id #=> 3
295
- ```
290
+ When a project grows, dependencies grows too. Keeping them into one single file
291
+ leads to headaches. One possible solution to keep everything under control is
292
+ to split your dependencies into many files.
296
293
 
297
- You can also create beans that contain single values. It will help you to avoid referencing global
298
- variables in your code.
294
+ To include a "sub-map" into another one, you can use `include_map` method.
299
295
 
300
296
  ```ruby
301
- map = Shirinji::Map.new do
302
- bean(:config, value: Proc.new { Application.config })
303
- bean(:foo, klass: 'Foo')
297
+ # dependencies/services.rb
298
+ Shirinji::Map.new do
299
+ bean(:foo_service, klass: 'FooService')
304
300
  end
305
301
 
306
- resolver = Shirinji::Resolver.new(map)
302
+ # dependencies/queries.rb
303
+ Shirinji::Map.new do
304
+ bean(:foo_query, klass: 'FooQuery')
305
+ end
307
306
 
308
- class Foo
309
- attr_reader :config
307
+ # dependencies.rb
308
+
309
+ root = Pathname.new(File.expand_path('../dependencies', __FILE__))
310
+
311
+ Shirinji::Map.new do
312
+ bean(:config, value: -> { MyApp.config })
310
313
 
311
- def initialize(config:)
312
- @config = config
313
- end
314
+ # paths must be absolute
315
+ include_map(root.join('queries.rb'))
316
+ include_map(root.join('services.rb'))
314
317
  end
315
-
316
- resolver.resolve(:foo)
317
- #=> <#Foo @config=<#OpenStruct ...> ...>
318
318
  ```
319
319
 
320
- Values can be anything. A `Proc` will be lazily evaluated. They also obey the singleton / instance
321
- strategy.
322
-
323
320
  ## Notes
324
321
 
325
- - It is absolutely mandatory for your beans to be stateless to use the singleton mode. If they're
326
- not, you will probably run into trouble as your objects behavior will depend on their history, leading
327
- to unpredictable effects.
328
- - Shirinji only works with named arguments. It will raise errors if you try to use it with "standard"
329
- method arguments.
322
+ - It is absolutely mandatory for your beans to be stateless to use the singleton
323
+ mode. If they're not, you will probably run into trouble as your objects
324
+ behavior will depend on their history, leading to unpredictable effects.
325
+ - Shirinji only works with named arguments. It will raise `ArgumentError` if you
326
+ try to use it with "standard" method arguments.
327
+
328
+ ## TODOS
329
+
330
+ - solve absolute paths problems for `include_map` (`instance_eval` is a problem)
330
331
 
331
332
  ## Contributing
332
333
 
333
- Bug reports and pull requests are welcome on GitHub at https://github.com/fdutey/shirinji.
334
+ Bug reports and pull requests are welcome on GitHub at
335
+ https://github.com/fdutey/shirinji.
data/lib/shirinji/bean.rb CHANGED
@@ -4,6 +4,7 @@ module Shirinji
4
4
  class Bean
5
5
  attr_reader :name, :class_name, :value, :access, :attributes, :construct
6
6
 
7
+ # rubocop:disable Metrics/ParameterLists
7
8
  def initialize(
8
9
  name, class_name: nil, value: nil, access:, construct: true, &block
9
10
  )
@@ -18,6 +19,7 @@ module Shirinji
18
19
 
19
20
  instance_eval(&block) if block
20
21
  end
22
+ # rubocop:enable Metrics/ParameterLists
21
23
 
22
24
  def attr(name, ref:)
23
25
  attributes[name] = Attribute.new(name, ref)
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,7 +72,7 @@ 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
51
76
  # @option [Boolean] :construct whether the bean should be constructed or not
52
77
  # @option [Symbol] :access either :singleton or :instance.
53
78
  # @yield additional method to construct our bean
@@ -59,7 +84,7 @@ module Shirinji
59
84
 
60
85
  options = others.merge(
61
86
  access: access,
62
- class_name: klass && klass.freeze
87
+ class_name: klass&.freeze
63
88
  )
64
89
 
65
90
  beans[name] = Bean.new(name, **options, &block)
@@ -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
@@ -71,6 +71,7 @@ module Shirinji
71
71
  def check_params!(params)
72
72
  params.each do |pair|
73
73
  next if ARG_TYPES.include?(pair.first)
74
+
74
75
  raise ArgumentError, 'Only key arguments are allowed'
75
76
  end
76
77
  end
@@ -2,39 +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, **others, &block)
22
- chunks = [mod, "#{klass}#{klass_suffix}"].compact
23
- options = others.merge(klass: klass ? chunks.join('::') : nil)
24
- scoped_name = [prefix, name, suffix].compact.join('_')
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)
25
33
 
26
34
  parent.bean(scoped_name, **options, &block)
27
35
  end
28
36
 
29
37
  def scope(**options, &block)
30
- 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)
31
45
  end
32
46
 
33
47
  private
34
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
+
35
81
  def validate_options(args)
36
82
  args.each_key do |k|
37
83
  next if Shirinji::Scope::VALID_OPTIONS.include?(k)
84
+
38
85
  raise ArgumentError, "Unknown key #{k}"
39
86
  end
40
87
  end
@@ -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
@@ -0,0 +1,6 @@
1
+ module Shirinji
2
+ module Utils
3
+ end
4
+ end
5
+
6
+ require_relative 'utils/string'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shirinji
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.4'
5
5
  end
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'
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
24
  spec.add_development_dependency 'rake', '~> 10.0'
27
25
  spec.add_development_dependency 'rspec', '~> 3.0'
28
26
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shirinji
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Dutey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-04-11 00:00:00.000000000 Z
11
+ date: 2019-08-23 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
@@ -59,6 +45,7 @@ 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"
@@ -78,6 +65,8 @@ files:
78
65
  - lib/shirinji/map.rb
79
66
  - lib/shirinji/resolver.rb
80
67
  - lib/shirinji/scope.rb
68
+ - lib/shirinji/utils.rb
69
+ - lib/shirinji/utils/string.rb
81
70
  - lib/shirinji/version.rb
82
71
  - shirinji.gemspec
83
72
  homepage: https://github.com/fdutey/shirinji