shirinji 0.0.3 → 0.0.4

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
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