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