ruse 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: eea82ddd26c784db349a59f881be20f9137061bc
4
- data.tar.gz: a9612d3e8d4802eb5291a2615b2ebba9d8f76343
3
+ metadata.gz: d095e3c4fbabc172f422463a08a3509906cd45cd
4
+ data.tar.gz: 511b955cccf7a731dec5b94f2fdda97ad6bdd6a1
5
5
  SHA512:
6
- metadata.gz: ba15491b3261228c622116eb269594b9bd77b5e1910e9146fc8821ddff10eff6c05c7a8fd691098b3952a5bc4e9894d0b6b7fd2018027e6124aade947fe29a5c
7
- data.tar.gz: d6a1d5057fe6ffa7a144d5998402b92a53b5d487b51b19745c72730517243b5e0e3d7b8452ffb5b0022f204db2a5286f5402991c1a379f9a4db333b80b3a5558
6
+ metadata.gz: 8c12916ebcd87eb57c84f6ab9f965323942fea70b3fbc49b69c184d3e7b5af957312908dedc8f0d4914391593ebe6c9be20745298d057381e22b15de0c0876b7
7
+ data.tar.gz: 37ffdd78470a69c001b778688a94dabec4d6229c28306a7c977107b4cab8380eba569347f83d23f1bc9e7ad06d3085dff9d5b6f12a3d0e63a5dfc540c0cd9863
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0
6
+ - 2.1
data/README.md CHANGED
@@ -82,19 +82,86 @@ all the way down the object graph.
82
82
 
83
83
  ## Instance Resolution
84
84
 
85
- Dependencies are determined by the identifiers used in constructure parameters.
85
+ Dependencies are determined by the identifiers used in constructor parameters.
86
86
  This was lifted directly from angular.js, and I believe may be the key to
87
87
  reducing the overhead in using a tool like this. Your dependency consuming
88
88
  classes do not have to be annotated or registered in any way.
89
89
 
90
- In this early alpha state, identifiers are resolved to types through simple
90
+ By default, identifiers are resolved to types through simple
91
91
  string manipulation (similiar to ActiveSupport's `classify` and `constantize`).
92
92
  That means you can get an instance of `SomeService` by requesting
93
- `"SomeService"`, `"some_service"` or `:some_service`.
93
+ `"SomeService"`, `"some_service"` or `:some_service`. The type is then
94
+ instantiated (populating all of *its* dependencies using the same mechanism)
95
+ and passed in to the constructor.
94
96
 
95
- In the future, I can imagine a simple configuration mechanism that lets you
96
- resolve an identifier to some other type, so `"notifier"` resolves to
97
- `EmailNotifier`.
97
+ However, you can configure the injector to use types that differ from the
98
+ parameter name, or return existing objects.
99
+
100
+ ## Instance Lifecycle
101
+
102
+ Currently, all objects retrieved from the injector are treated as singletons.
103
+ This means that any time you ask a given injector for an instance of a
104
+ service, you will always get the same exact instance. If you want a new
105
+ instance, you need to use a new instance of the injector. In the future,
106
+ the lifecycle may be configurable, but I haven't needed it yet.
107
+
108
+ ## Configuration
109
+
110
+ Currently, you configure the injector by passing an options `Hash` to
111
+ `Ruse.create_injector` or to the `#configure` method of an `Injector` instance.
112
+ You can call the `#configure` method multiple times, and each time the options
113
+ will be merged into the existing options.
114
+
115
+ Eventually there may be an API or DSL for building the options `Hash`, but for
116
+ now, you need to know the specific keys that are understood internally.
117
+
118
+ ### Aliases
119
+
120
+ Aliases are the most common configuration. They allow you to specify the type
121
+ that should be injected for a given parameter name. In the example above,
122
+ the `CreateOrderCommand` relies on a `:notifier`. By default, Ruse will
123
+ attempt to inject an instance of `Notifier`. However, you may have two services
124
+ that can act as a `notifier`: `EmailNotifier` or `FileSystemNotifier`.
125
+
126
+ In production, you want real emails to be sent, so you would configure the
127
+ injector to use `EmailNotifier`:
128
+
129
+ ```ruby
130
+ Ruse.create_injector aliases: {notifier: "EmailNotifier"}
131
+ ```
132
+
133
+ However, in testing, you just want to record notifications in the filesystem:
134
+
135
+ ```ruby
136
+ Ruse.create_injector aliases: {notifier: "FileSystemNotifier"}
137
+ ```
138
+
139
+ ### Values
140
+
141
+ Sometimes you want to inject a specific value, or existing object instance,
142
+ instead of relying on the injector to create the instance.
143
+
144
+ ```ruby
145
+ Ruse.create_injector values: {max_uploads: 42, file_system: File}
146
+ ```
147
+
148
+ You could now create a class that depends on `file_system` and send messages
149
+ exposed by `File`, without an explicit dependency on `File` (making it easy
150
+ to pass in a stub file system during tests).
151
+
152
+ ### Factories / Procs / Delayed Evaluation
153
+
154
+ Factories are similar to `values`, but allow you to delay the creation of the
155
+ object until it is requested. For example, you might want to inject a
156
+ connection to a 3rd party service, but you don't want to create the connection
157
+ at configuration time - you want to wait until it is used.
158
+
159
+ ```ruby
160
+ Ruse.create_injector factories: {
161
+ s3_connection:
162
+ ->{ AWS::S3.new(config).buckets["my_files"] }
163
+ }
164
+ ```
98
165
 
99
166
  ## Installation
100
167
 
@@ -1,6 +1,12 @@
1
+ require 'ruse/proc_resolver'
2
+ require 'ruse/type_resolver'
3
+ require 'ruse/value_resolver'
4
+ require 'ruse/object_factory'
5
+
1
6
  module Ruse
2
7
  class Injector
3
8
  def get(identifier)
9
+ ensure_valid_identifier! identifier
4
10
  identifier = aliases[identifier] || identifier
5
11
  cache_fetch(identifier) do
6
12
  resolver = find_resolver identifier
@@ -10,11 +16,32 @@ module Ruse
10
16
  end
11
17
 
12
18
  def configure(settings)
13
- configuration.merge! settings
19
+ configuration.each do |key, hsh|
20
+ hsh.merge!(settings[key] || {})
21
+ end
22
+ end
23
+
24
+ def can_resolve?(identifier)
25
+ return false if invalid_identifier?(identifier)
26
+ find_resolver(identifier) ? true : false
14
27
  end
15
28
 
16
29
  private
17
30
 
31
+ def ensure_valid_identifier!(identifier)
32
+ if invalid_identifier? identifier
33
+ raise InvalidServiceName.new("<#{identifier.inspect}>")
34
+ end
35
+ end
36
+
37
+ def invalid_identifier?(identifier)
38
+ identifier.nil? || empty_string?(identifier)
39
+ end
40
+
41
+ def empty_string?(s)
42
+ s.is_a?(String) && s !~ /[^[:space:]]/
43
+ end
44
+
18
45
  def cache_fetch(identifier, &block)
19
46
  return cache[identifier] if cache.key?(identifier)
20
47
  cache[identifier] = block.call
@@ -32,6 +59,17 @@ module Ruse
32
59
  }
33
60
  end
34
61
 
62
+ def reset_configuration
63
+ @configuration = nil
64
+ end
65
+ # Allow #initialize_clone to reset the configuration of the cloned injector
66
+ protected :reset_configuration
67
+
68
+ def initialize_clone(cloned_injector)
69
+ cloned_injector.reset_configuration
70
+ cloned_injector.configure(configuration)
71
+ end
72
+
35
73
  def aliases
36
74
  configuration[:aliases]
37
75
  end
@@ -60,98 +98,5 @@ module Ruse
60
98
  end
61
99
 
62
100
  class UnknownServiceError < StandardError; end
63
-
64
- class ProcResolver
65
- attr_reader :factories
66
- def initialize(factories)
67
- @factories = factories
68
- end
69
- def can_build?(identifier)
70
- factories.key? identifier
71
- end
72
-
73
- def build(identifier)
74
- factory = factories.fetch(identifier)
75
- factory.call
76
- end
77
- end
78
-
79
- class ValueResolver
80
- attr_reader :values
81
-
82
- def initialize(values)
83
- @values = values
84
- end
85
-
86
- def can_build?(identifier)
87
- values.key? identifier
88
- end
89
-
90
- def build(identifier)
91
- values.fetch(identifier)
92
- end
93
- end
94
-
95
- class TypeResolver
96
- def initialize(injector)
97
- @injector = injector
98
- end
99
-
100
- def can_build?(identifier)
101
- type_name = self.class.classify(identifier)
102
- load_type type_name
103
- end
104
-
105
- def build(identifier)
106
- type = resolve_type identifier
107
- object_factory.build(type)
108
- end
109
-
110
- def self.classify(term)
111
- # lifted from active_support gem: lib/active_support/inflector/methods.rb
112
- string = term.to_s
113
- string = string.sub(/^[a-z\d]*/) { $&.capitalize }
114
- string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{ $2.capitalize}" }.gsub('/', '::')
115
- end
116
-
117
- private
118
-
119
- def load_type(type_name)
120
- type_name.split('::').reduce(Object){|ns, name|
121
- if ns.const_defined? name
122
- ns.const_get name
123
- end
124
- }
125
- end
126
-
127
- def object_factory
128
- @object_factory ||= ObjectFactory.new(@injector)
129
- end
130
-
131
- def resolve_type(identifier)
132
- type_name = self.class.classify(identifier)
133
- load_type type_name
134
- end
135
- end
136
-
137
- class ObjectFactory
138
- attr_reader :injector
139
-
140
- def initialize(injector)
141
- @injector = injector
142
- end
143
-
144
- def build(type)
145
- args = resolve_dependencies type
146
- type.new *args
147
- end
148
-
149
- private
150
-
151
- def resolve_dependencies(type)
152
- type.instance_method(:initialize).parameters.map{|_, identifier|
153
- @injector.get identifier
154
- }
155
- end
156
- end
101
+ class InvalidServiceName < StandardError; end
157
102
  end
@@ -0,0 +1,94 @@
1
+ module Ruse
2
+ class ObjectFactory
3
+ attr_reader :injector
4
+
5
+ def initialize(injector)
6
+ @injector = injector
7
+ end
8
+
9
+ def build(type)
10
+ initializer = Initializer.new @injector, type.instance_method(:initialize)
11
+ initializer.resolve_dependencies!
12
+ type.new *initializer.args
13
+ end
14
+
15
+ class Initializer
16
+ attr_reader :positional_args, :keyword_args
17
+
18
+ def initialize(injector, initialize_method)
19
+ @injector = injector
20
+ @initialize_method = initialize_method
21
+ @positional_args = []
22
+ @keyword_args = {}
23
+ end
24
+
25
+ def args
26
+ [*positional_args, keyword_args].tap do |list|
27
+ list.pop if list.last.empty?
28
+ end
29
+ end
30
+
31
+ def resolve_dependencies!
32
+ @initialize_method.parameters.each do |arg_type, identifier|
33
+ MethodArgument.build(arg_type, identifier, @injector).resolve(self)
34
+ end
35
+ end
36
+
37
+ MethodArgument = Struct.new :arg_type, :identifier, :injector do
38
+ def self.build(arg_type, *args)
39
+ [PositionalArgument, KeywordArgument].each do |klass|
40
+ return klass.new(arg_type, *args) if klass.match?(arg_type)
41
+ end
42
+ UnhandleableArgument
43
+ end
44
+
45
+ def build_dependency
46
+ injector.get identifier
47
+ end
48
+
49
+ def must_resolve?
50
+ required? || injector.can_resolve?(identifier)
51
+ end
52
+
53
+ def resolve(initializer)
54
+ resolve!(initializer) if must_resolve?
55
+ end
56
+ end
57
+
58
+ class PositionalArgument < MethodArgument
59
+ def self.match?(arg_type)
60
+ [:req, :opt].include? arg_type
61
+ end
62
+
63
+ def required?
64
+ arg_type == :req
65
+ end
66
+
67
+ def resolve!(initializer)
68
+ initializer.positional_args << build_dependency
69
+ end
70
+ end
71
+
72
+ class KeywordArgument < MethodArgument
73
+ def self.match?(arg_type)
74
+ [:key, :keyreq].include? arg_type
75
+ end
76
+
77
+ def required?
78
+ arg_type == :keyreq
79
+ end
80
+
81
+ def resolve!(initializer)
82
+ initializer.keyword_args[identifier] = build_dependency
83
+ end
84
+ end
85
+
86
+ # Singleton object that be substituted for the other MethodArgument
87
+ # subclasses when we don't want to try resolving the argument.
88
+ module UnhandleableArgument
89
+ extend self
90
+ def resolve(*) end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,14 @@
1
+ class ProcResolver
2
+ attr_reader :factories
3
+ def initialize(factories)
4
+ @factories = factories
5
+ end
6
+ def can_build?(identifier)
7
+ factories.key? identifier
8
+ end
9
+
10
+ def build(identifier)
11
+ factory = factories.fetch(identifier)
12
+ factory.call
13
+ end
14
+ end
@@ -0,0 +1,47 @@
1
+ module Ruse
2
+ class TypeResolver
3
+ def initialize(injector)
4
+ @injector = injector
5
+ end
6
+
7
+ def can_build?(identifier)
8
+ type_name = self.class.classify(identifier)
9
+ load_type type_name
10
+ end
11
+
12
+ def build(identifier)
13
+ type = resolve_type identifier
14
+ object_factory.build(type)
15
+ end
16
+
17
+ def self.classify(term)
18
+ # lifted from active_support gem: lib/active_support/inflector/methods.rb
19
+ string = term.to_s
20
+ string = string.sub(/^[a-z\d]*/) { $&.capitalize }
21
+ string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{ $2.capitalize}" }.gsub('/', '::')
22
+ end
23
+
24
+ private
25
+
26
+ def base_module
27
+ Object
28
+ end
29
+
30
+ def load_type(type_name)
31
+ type_name.split('::').reduce(base_module){|ns, name|
32
+ if ns.const_defined? name
33
+ ns.const_get name
34
+ end
35
+ }
36
+ end
37
+
38
+ def object_factory
39
+ @object_factory ||= ObjectFactory.new(@injector)
40
+ end
41
+
42
+ def resolve_type(identifier)
43
+ type_name = self.class.classify(identifier)
44
+ load_type type_name
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ module Ruse
2
+ class ValueResolver
3
+ attr_reader :values
4
+
5
+ def initialize(values)
6
+ @values = values
7
+ end
8
+
9
+ def can_build?(identifier)
10
+ values.key? identifier
11
+ end
12
+
13
+ def build(identifier)
14
+ values.fetch(identifier)
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module Ruse
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -18,6 +18,6 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_development_dependency "bundler", "~> 1.6"
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
22
  spec.add_development_dependency "rake"
23
23
  end
@@ -0,0 +1,75 @@
1
+ require 'minitest/spec'
2
+ require 'minitest/autorun'
3
+ require 'ruse/injector'
4
+
5
+ describe Ruse::Injector do
6
+ def injector
7
+ @injector ||= Ruse::Injector.new
8
+ end
9
+
10
+ it "injects keyword arguments it can resolve, delegating to defaults when it can't" do
11
+ skip("No keyword argument support before Ruby 2.0") unless RUBY_VERSION >= "2.0"
12
+ object = injector.get("HasKeywordArguments")
13
+ object.a.must_be_kind_of ServiceA
14
+ object.z.must_equal :z
15
+ end
16
+
17
+ it "injects optional parameters it can resolve, delegating to defaults when it can't" do
18
+ skip("No keyword argument support before Ruby 2.0") unless RUBY_VERSION >= "2.0"
19
+ object = injector.get("HasOptionalParameters")
20
+ object.a.must_be_kind_of ServiceA
21
+ object.z.must_equal :z
22
+ end
23
+
24
+ it "injects required keyword arguments" do
25
+ skip("No required keyword argument support before Ruby 2.1") unless RUBY_VERSION >= "2.1"
26
+ object = injector.get("HasRequiredKeywordArguments")
27
+ object.a.must_be_kind_of ServiceA
28
+ end
29
+
30
+ it "exceptions when a required keyword argument can't resolve" do
31
+ skip("No required keyword argument support before Ruby 2.1") unless RUBY_VERSION >= "2.1"
32
+ -> {
33
+ injector.get("HasUnresolvableRequiredKeywordArguments")
34
+ }.must_raise Ruse::UnknownServiceError
35
+ end
36
+
37
+ class ServiceA; end
38
+
39
+ if RUBY_VERSION >= "2.0"
40
+ class HasOptionalParameters
41
+ attr_reader :a, :z
42
+ class_eval <<-EVAL, __FILE__, __LINE__
43
+ def initialize(service_a = :a, service_z = :z)
44
+ @a = service_a
45
+ @z = service_z
46
+ end
47
+ EVAL
48
+ end
49
+
50
+ class HasKeywordArguments
51
+ attr_reader :a, :z
52
+ class_eval <<-EVAL, __FILE__, __LINE__
53
+ def initialize(service_a: :a, service_z: :z)
54
+ @a = service_a
55
+ @z = service_z
56
+ end
57
+ EVAL
58
+ end
59
+ end
60
+ if RUBY_VERSION >= "2.1"
61
+ class HasRequiredKeywordArguments
62
+ attr_reader :a
63
+ class_eval <<-EVAL, __FILE__, __LINE__
64
+ def initialize(service_a:)
65
+ @a = service_a
66
+ end
67
+ EVAL
68
+ end
69
+ class HasUnresolvableRequiredKeywordArguments
70
+ class_eval <<-EVAL, __FILE__, __LINE__
71
+ def initialize(service_z:) end
72
+ EVAL
73
+ end
74
+ end
75
+ end
@@ -26,6 +26,27 @@ describe Ruse::Injector do
26
26
  }.must_raise(Ruse::UnknownServiceError)
27
27
  end
28
28
 
29
+ it "raises InvalidServiceName when identifier is nil" do
30
+ ->{
31
+ injector.get(nil)
32
+ }.must_raise(Ruse::InvalidServiceName)
33
+ end
34
+
35
+ it "raises InvalidServiceName when identifier is blank string" do
36
+ ->{
37
+ injector.get(" ")
38
+ }.must_raise(Ruse::InvalidServiceName)
39
+ end
40
+
41
+ it "cannot resolve a nil identifier" do
42
+ refute injector.can_resolve?(nil)
43
+ end
44
+
45
+ it "cannot resolve a blank string identifier" do
46
+ refute injector.can_resolve?(" ")
47
+ end
48
+
49
+
29
50
  it "populates dependencies for the instance it retrieves" do
30
51
  instance = injector.get("ConsumerA")
31
52
  instance.must_be_instance_of(ConsumerA)
@@ -70,6 +91,29 @@ describe Ruse::Injector do
70
91
  instance.value.must_equal(88)
71
92
  end
72
93
 
94
+ it "skips over splats" do
95
+ injector.get("HasSplatInInitializer").must_be_kind_of HasSplatInInitializer
96
+ injector.get("Array").must_equal []
97
+ end
98
+
99
+ it "can be reconfigured" do
100
+ injector.configure(values: { service_a: 'foo' })
101
+ injector.configure(values: { service_b: 'bar' })
102
+ object = injector.get(:consumer_a)
103
+ object.a.must_equal 'foo'
104
+ object.b.must_equal 'bar'
105
+ end
106
+
107
+ it "duplicates configuration on cloning" do
108
+ injector.configure(values: { service_a: 'foo', service_b: 'bar' })
109
+ child_injector = injector.clone
110
+ child_injector.configure(values: { service_a: 'FOO' })
111
+ child_object = child_injector.get(:consumer_a)
112
+ parent_object = injector.get(:consumer_a)
113
+
114
+ child_object.a.must_equal 'FOO'
115
+ parent_object.a.must_equal 'foo'
116
+ end
73
117
 
74
118
  class ServiceA; end
75
119
  class ServiceB; end
@@ -111,6 +155,10 @@ describe Ruse::Injector do
111
155
  end
112
156
  end
113
157
 
158
+ class HasSplatInInitializer
159
+ def initialize(*args)
160
+ end
161
+ end
114
162
  end
115
163
 
116
164
  describe "classify" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruse
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
  - Joshua Flanagan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-28 00:00:00.000000000 Z
11
+ date: 2014-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -17,13 +17,13 @@ dependencies:
17
17
  requirements:
18
18
  - - ~>
19
19
  - !ruby/object:Gem::Version
20
- version: '1.6'
20
+ version: '1.5'
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ~>
25
25
  - !ruby/object:Gem::Version
26
- version: '1.6'
26
+ version: '1.5'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  type: :development
@@ -46,6 +46,7 @@ extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
48
  - .gitignore
49
+ - .travis.yml
49
50
  - Gemfile
50
51
  - LICENSE.txt
51
52
  - README.md
@@ -53,8 +54,13 @@ files:
53
54
  - lib/ruse.rb
54
55
  - lib/ruse/activesupport.rb
55
56
  - lib/ruse/injector.rb
57
+ - lib/ruse/object_factory.rb
58
+ - lib/ruse/proc_resolver.rb
59
+ - lib/ruse/type_resolver.rb
60
+ - lib/ruse/value_resolver.rb
56
61
  - lib/ruse/version.rb
57
62
  - ruse.gemspec
63
+ - specs/injector_keyword_args_spec.rb
58
64
  - specs/injector_spec.rb
59
65
  - specs/ruse_spec.rb
60
66
  homepage: https://github.com/joshuaflanagan/ruse