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