payload 0.1.0

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