payload 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/CONTRIBUTING.md +10 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +28 -0
- data/LICENSE +19 -0
- data/README.md +242 -0
- data/Rakefile +10 -0
- data/lib/payload/container.rb +96 -0
- data/lib/payload/controller.rb +12 -0
- data/lib/payload/decorator_chain.rb +23 -0
- data/lib/payload/definition_list.rb +41 -0
- data/lib/payload/exported_definition.rb +19 -0
- data/lib/payload/factory.rb +33 -0
- data/lib/payload/factory_definition.rb +24 -0
- data/lib/payload/mutable_container.rb +57 -0
- data/lib/payload/rack_container.rb +23 -0
- data/lib/payload/rails_loader.rb +58 -0
- data/lib/payload/railtie.rb +20 -0
- data/lib/payload/service_definition.rb +24 -0
- data/lib/payload/testing.rb +84 -0
- data/lib/payload/undefined_dependency_error.rb +5 -0
- data/lib/payload/version.rb +3 -0
- data/payload.gemspec +24 -0
- data/spec/payload/container_spec.rb +154 -0
- data/spec/payload/controller_spec.rb +32 -0
- data/spec/payload/decorator_chain_spec.rb +29 -0
- data/spec/payload/definition_list_spec.rb +80 -0
- data/spec/payload/exported_definition_spec.rb +30 -0
- data/spec/payload/factory_definition_spec.rb +49 -0
- data/spec/payload/factory_spec.rb +46 -0
- data/spec/payload/mutable_container_spec.rb +68 -0
- data/spec/payload/rack_container_spec.rb +33 -0
- data/spec/payload/rails_loader_spec.rb +62 -0
- data/spec/payload/service_definition_spec.rb +39 -0
- data/spec/spec_helper.rb +0 -0
- metadata +137 -0
@@ -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
|
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
|