payload 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ module Payload
2
+ # Collects a list of decorators to apply to a component within the context of
3
+ # a container.
4
+ #
5
+ # Used internally by {Container}. Use {Container#decorate}.
6
+ #
7
+ # @api private
8
+ class DecoratorChain
9
+ def initialize(decorators = [])
10
+ @decorators = decorators
11
+ end
12
+
13
+ def add(decorator)
14
+ self.class.new @decorators + [decorator]
15
+ end
16
+
17
+ def decorate(base, container)
18
+ @decorators.inject(base) do |component, decorator|
19
+ decorator.call(component, container)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,41 @@
1
+ require 'payload/exported_definition'
2
+ require 'payload/undefined_dependency_error'
3
+
4
+ module Payload
5
+ # Immutable list of definitions.
6
+ #
7
+ # Used internally by {Container} to define and look up definitions.
8
+ #
9
+ # @api private
10
+ class DefinitionList
11
+ def initialize(definitions = {})
12
+ @definitions = definitions
13
+ end
14
+
15
+ def add(name, definition)
16
+ self.class.new(definitions.merge(name => definition))
17
+ end
18
+
19
+ def find(name)
20
+ definitions.fetch(name) do
21
+ raise(UndefinedDependencyError, "No definition for dependency: #{name}")
22
+ end
23
+ end
24
+
25
+ def export(names)
26
+ exported_definitions = names.inject({}) do |result, name|
27
+ result.merge! name => ExportedDefinition.new(find(name), self)
28
+ end
29
+
30
+ self.class.new exported_definitions
31
+ end
32
+
33
+ def import(imports)
34
+ self.class.new definitions.merge(imports.definitions)
35
+ end
36
+
37
+ protected
38
+
39
+ attr_reader :definitions
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ module Payload
2
+ # Decorates a base definition such as {ServiceDefinition} to provide access to
3
+ # private dependencies in the {Container} from which the definition was
4
+ # exported.
5
+ #
6
+ # Used internally by {DefinitionList}. Use {Container#export}.
7
+ #
8
+ # @api private
9
+ class ExportedDefinition
10
+ def initialize(definition, private_definitions)
11
+ @definition = definition
12
+ @private_definitions = private_definitions
13
+ end
14
+
15
+ def resolve(container)
16
+ @definition.resolve(container.import(@private_definitions))
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ module Payload
2
+ # Returned by {Container#[]} for {Container#factory} definitions.
3
+ #
4
+ # Used to add definitions from {#new} to the {Container} and then run the
5
+ # factory definition block to obtain an instance.
6
+ #
7
+ # @see Container#factory Container#factory for defining and using factories.
8
+ class Factory
9
+ # Used internally by {FactoryDefinition}.
10
+ #
11
+ # @api private
12
+ def initialize(container, block, decorators)
13
+ @container = container
14
+ @block = block
15
+ @decorators = decorators
16
+ end
17
+
18
+ # @param [Hash] arguments additional dependencies and arguments to resolve
19
+ # before invoking the factory definition block.
20
+ # @return the instance defined by the factory definition block.
21
+ # @see Container#factory Container#factory for defining and using factories.
22
+ def new(arguments = {})
23
+ resolved =
24
+ arguments.inject(@container) do |container, (argument, value)|
25
+ container.service(argument) { value }
26
+ end
27
+
28
+ base = @block.call(resolved)
29
+
30
+ @decorators.decorate(base, resolved)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ require 'payload/decorator_chain'
2
+ require 'payload/factory'
3
+
4
+ module Payload
5
+ # Encapsulates logic for resolving factory definitions.
6
+ #
7
+ # Used internally by {Container}. Use {Container#factory}.
8
+ #
9
+ # @api private
10
+ class FactoryDefinition
11
+ def initialize(block, decorators = DecoratorChain.new)
12
+ @block = block
13
+ @decorators = decorators
14
+ end
15
+
16
+ def resolve(container)
17
+ Factory.new(container, @block, @decorators)
18
+ end
19
+
20
+ def decorate(block)
21
+ self.class.new(@block, @decorators.add(block))
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ module Payload
2
+ # Mutable builder for defining dependencies.
3
+ #
4
+ # Allows defining dependencies without fear of breaking the chain while still
5
+ # encapsulating mutation in one location.
6
+ #
7
+ # Decorates a {Container} and delegates definition calls.
8
+ class MutableContainer
9
+ # Used internally by {RailsLoader} to parse dependency definition files.
10
+ #
11
+ # @api private
12
+ def initialize(container)
13
+ @container = container
14
+ @exported_names = []
15
+ end
16
+
17
+ # Delegates to {Container} and uses the returned result as the new
18
+ # container.
19
+ # @!method decorate
20
+ # @!method factory
21
+ # @!method service
22
+ def method_missing(*args, &block)
23
+ @container = @container.send(*args, &block)
24
+ self
25
+ end
26
+
27
+ # Used internally by {RailsLoader} to return the configured container.
28
+ #
29
+ # @api private
30
+ # @return Container the fully-configured, immutable container.
31
+ def build
32
+ @container
33
+ end
34
+
35
+ # Exports dependencies so that they are available in other containers.
36
+ #
37
+ # @param names [Array<Symbol>] dependencies to export.
38
+ def export(*names)
39
+ @exported_names += names
40
+ end
41
+
42
+ # Returns dependencies specified by previous {#export} invocations.
43
+ #
44
+ # Used internally by {RailsLoader}.
45
+ #
46
+ # @api private
47
+ def exports
48
+ @container.export(*@exported_names)
49
+ end
50
+
51
+ private
52
+
53
+ def respond_to_missing?(*args)
54
+ @container.respond_to?(*args)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,23 @@
1
+ module Payload
2
+ # Uses a dependency loader to load dependencies and inject them into the Rack
3
+ # environment.
4
+ #
5
+ # Accepts a Rack application and a block for loading dependencies as a
6
+ # {Container}. The container will be injected into each Rack request as
7
+ # +:dependencies+ in the Rack environment.
8
+ #
9
+ # Used internally by {Railtie}.
10
+ #
11
+ # @api private
12
+ class RackContainer
13
+ def initialize(app, &loader)
14
+ @app = app
15
+ @loader = loader
16
+ end
17
+
18
+ def call(env)
19
+ env[:dependencies] = @loader.call.service(:rack_env) { |container| env }
20
+ @app.call(env)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,58 @@
1
+ require 'payload/definition_list'
2
+ require 'payload/container'
3
+ require 'payload/mutable_container'
4
+
5
+ module Payload
6
+ # Loads dependencies from config/dependencies.rb in Rails applications.
7
+ #
8
+ # Used by {Railtie} to provide a Rails dependency loader to {RackContainer}.
9
+ class RailsLoader
10
+ # @api private
11
+ def self.to_proc
12
+ lambda { load }
13
+ end
14
+
15
+ # Load dependencies from outside a Rails request.
16
+ # @example
17
+ # RailsLoader.load[:example_service]
18
+ # @return [Container] dependencies from config/dependencies.rb
19
+ def self.load
20
+ new.load
21
+ end
22
+
23
+ # @api private
24
+ def load
25
+ namespace_containers.inject(root_container) do |target, source|
26
+ target.import(source.exports)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def namespace_containers
33
+ namespace_config_paths.map { |path| load_from(path) }
34
+ end
35
+
36
+ def root_container
37
+ load_from(root_config_path).build
38
+ end
39
+
40
+ def load_from(path)
41
+ container = MutableContainer.new(Container.new(DefinitionList.new))
42
+ container.instance_eval(IO.read(path), path)
43
+ container
44
+ end
45
+
46
+ def root_config_path
47
+ config_path.join('dependencies.rb').to_s
48
+ end
49
+
50
+ def namespace_config_paths
51
+ Dir.glob config_path.join('payload/*.rb').to_s
52
+ end
53
+
54
+ def config_path
55
+ Rails.root.join('config')
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,20 @@
1
+ require 'payload/rack_container'
2
+ require 'payload/rails_loader'
3
+ require 'payload/controller'
4
+
5
+ module Payload
6
+ # Automatically loads and injects dependencies into the Rack environment for
7
+ # Rails applications.
8
+ #
9
+ # Requiring this file is enough to:
10
+ #
11
+ # * Load dependency definitions from config/dependencies.rb using
12
+ # {RailsLoader} and {MutableContainer}.
13
+ # * Provide a {Container} in each Rack request.
14
+ class Railtie < ::Rails::Railtie
15
+ config.app_middleware.use(
16
+ Payload::RackContainer,
17
+ &Payload::RailsLoader
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ require 'payload/decorator_chain'
2
+
3
+ module Payload
4
+ # Encapsulates logic for resolving service definitions.
5
+ #
6
+ # Used internally by {Container}. Use {Container#service}.
7
+ #
8
+ # @api private
9
+ class ServiceDefinition
10
+ def initialize(block, decorators = DecoratorChain.new)
11
+ @block = block
12
+ @decorators = decorators
13
+ end
14
+
15
+ def resolve(container)
16
+ base = @block.call(container)
17
+ @decorators.decorate(base, container)
18
+ end
19
+
20
+ def decorate(block)
21
+ self.class.new(@block, @decorators.add(block))
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,84 @@
1
+ require 'payload/definition_list'
2
+ require 'payload/container'
3
+
4
+ module Payload
5
+ # Helper methods for stubbing and injecting dependencies into unit tests.
6
+ #
7
+ # These methods are intended for rspec controller tests and require
8
+ # rspec-mocks to work as expected. During capybara tests, all dependencies
9
+ # are available as defined in +config/dependencies.rb+.
10
+ module Testing
11
+ # Builds an empty {Container} in the fake Rack environment.
12
+ #
13
+ # @api private
14
+ def setup_controller_request_and_response
15
+ super
16
+ @request.env[:dependencies] = build_container
17
+ end
18
+
19
+ # Finds or injects a stubbed factory into the test {Container} and stubs an
20
+ # instance to be created with the given attributes.
21
+ #
22
+ # @param dependency [Symbol] the name of the factory to stub.
23
+ # @param attributes [Hash] the expected attributes to build the instance.
24
+ # @return [RSpec::Mocks::TestDouble] the result of the factory, which can
25
+ # then have additional stubs or expectations applied to it.
26
+ def stub_factory_instance(dependency, attributes)
27
+ factory = stub_factory(dependency)
28
+ double(dependency.to_s).tap do |double|
29
+ factory.stub(:new).with(attributes).and_return(double)
30
+ end
31
+ end
32
+
33
+ # Injects a stubbed factory into the test {Container} and returns it.
34
+ #
35
+ # @param dependency [Symbol] the name of the factory to stub.
36
+ # @return [RSpec::Mocks::TestDouble] the stubbed factory, which can have
37
+ # stubs for `new` applied to it.
38
+ def stub_factory(dependency)
39
+ dependencies[dependency]
40
+ rescue Payload::UndefinedDependencyError
41
+ double("#{dependency} factory").tap do |factory|
42
+ modify_dependencies do |dependencies|
43
+ dependencies.service(dependency) do |config|
44
+ factory
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # Injects a stubbed service into the test {Container} and returns it.
51
+ #
52
+ # @param dependency [Symbol] the name of the service to stub.
53
+ # @return [RSpec::Mocks::TestDouble] the stubbed service, which can then
54
+ # have additional stubs or expectations applied to it.
55
+ def stub_service(dependency)
56
+ double(dependency.to_s).tap do |double|
57
+ modify_dependencies do |dependencies|
58
+ dependencies.service(dependency) do |config|
59
+ double
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # Convenience for injecting a modified container into the test rack session.
66
+ #
67
+ # @yield [Container] the container to be modified. The block should return
68
+ # the new container.
69
+ def modify_dependencies
70
+ @request.env[:dependencies] = yield(dependencies)
71
+ end
72
+
73
+ # @return the current {Container} which will be injected into the test Rack
74
+ # session.
75
+ def dependencies
76
+ @request.env[:dependencies]
77
+ end
78
+
79
+ # @api private
80
+ def build_container
81
+ Container.new
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ module Payload
2
+ # Raised when attempting to resolve an undefined dependency.
3
+ class UndefinedDependencyError < StandardError
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Payload
2
+ VERSION = '0.1.0'
3
+ end
data/payload.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+ require 'payload/version'
3
+ require 'date'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.authors = ['thoughtbot', 'Joe Ferris']
7
+ s.description = 'Dependency configuration and injection for Ruby and Rails.'
8
+ s.email = 'support@thoughtbot.com'
9
+ s.extra_rdoc_files = %w(LICENSE README.md CONTRIBUTING.md)
10
+ s.files = `git ls-files`.split("\n")
11
+ s.homepage = 'http://github.com/thoughtbot/payload'
12
+ s.license = 'MIT'
13
+ s.name = %q{payload}
14
+ s.rdoc_options = ['--charset=UTF-8']
15
+ s.require_paths = ['lib']
16
+ s.required_ruby_version = Gem::Requirement.new('>= 2.0.0')
17
+ s.summary = 'Dependency configuration and injection for Ruby and Rails.'
18
+ s.test_files = `git ls-files -- spec/*`.split("\n")
19
+ s.version = Payload::VERSION
20
+
21
+ s.add_development_dependency 'rack'
22
+ s.add_development_dependency 'rake'
23
+ s.add_development_dependency 'rspec', '~> 2.14.1'
24
+ end