├── Gemfile
├── .rspec
├── lib
├── manufacturable
│ ├── version.rb
│ ├── object_factory.rb
│ ├── railtie.rb
│ ├── dispatcher.rb
│ ├── config.rb
│ ├── injector.rb
│ ├── simple_registrar.rb
│ ├── factory.rb
│ ├── item.rb
│ ├── registrar.rb
│ └── builder.rb
└── manufacturable.rb
├── assets
└── factory.png
├── spec
├── support
│ └── rails.rb
├── spec_helper.rb
├── manufacturable
│ ├── object_factory_spec.rb
│ ├── railtie_spec.rb
│ ├── config_spec.rb
│ ├── registrar_spec.rb
│ ├── factory_spec.rb
│ ├── item_spec.rb
│ └── builder_spec.rb
├── manufacturable_spec.rb
└── integration
│ └── manufacturable_spec.rb
├── Rakefile
├── bin
├── setup
└── console
├── .gitignore
├── .travis.yml
├── .github
└── workflows
│ └── ruby.yml
├── LICENSE.txt
├── manufacturable.gemspec
├── CODE_OF_CONDUCT.md
└── README.md
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format progress
2 | --color
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/lib/manufacturable/version.rb:
--------------------------------------------------------------------------------
1 | module Manufacturable
2 | VERSION = "2.2.0"
3 | end
4 |
--------------------------------------------------------------------------------
/assets/factory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/first-try-software/manufacturable/HEAD/assets/factory.png
--------------------------------------------------------------------------------
/spec/support/rails.rb:
--------------------------------------------------------------------------------
1 | module Rails
2 | class Railtie
3 | def self.initializer(config, &block)
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rspec/core/rake_task"
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task :default => :spec
7 |
--------------------------------------------------------------------------------
/lib/manufacturable/object_factory.rb:
--------------------------------------------------------------------------------
1 | module Manufacturable
2 | class ObjectFactory
3 | extend Factory
4 |
5 | manufactures Object
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 |
10 | # rspec failure tracking
11 | .rspec_status
12 |
13 | Gemfile.lock
14 | .DS_Store
15 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 | SimpleCov.start do
3 | add_filter '/spec/'
4 | end
5 |
6 | require "bundler/setup"
7 | require "manufacturable"
8 |
9 | RSpec.configure do |config|
10 | config.example_status_persistence_file_path = ".rspec_status"
11 |
12 | config.disable_monkey_patching!
13 |
14 | config.expect_with :rspec do |c|
15 | c.syntax = :expect
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 | require "manufacturable"
5 |
6 | # You can add fixtures and/or initialization code here to make experimenting
7 | # with your gem easier. You can also use a different console, if you like.
8 |
9 | # (If you use this, don't forget to add pry to your Gemfile!)
10 | # require "pry"
11 | # Pry.start
12 |
13 | require "irb"
14 | IRB.start(__FILE__)
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: ruby
3 | cache: bundler
4 | rvm:
5 | - 2.7.1
6 | before_install: gem install bundler -v 2.1.4
7 | before_script:
8 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
9 | - chmod +x ./cc-test-reporter
10 | - ./cc-test-reporter before-build
11 | script:
12 | - bundle exec rspec
13 | after_script:
14 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
--------------------------------------------------------------------------------
/lib/manufacturable/railtie.rb:
--------------------------------------------------------------------------------
1 | require 'manufacturable/config'
2 |
3 | module Manufacturable
4 | class Railtie
5 | def self.load
6 | load_railtie if rails_defined?
7 | end
8 |
9 | def self.load_railtie
10 | Class.new(Rails::Railtie).initializer('manufacturable.require_paths') do |app|
11 | Manufacturable::Config.require_method = app.config.eager_load ? :require : :require_dependency
12 | end
13 | end
14 |
15 | def self.rails_defined?
16 | defined?(Rails)
17 | end
18 | end
19 | end
20 |
21 | Manufacturable::Railtie.load
--------------------------------------------------------------------------------
/spec/manufacturable/object_factory_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Manufacturable::ObjectFactory do
2 | subject(:factory) { described_class }
3 |
4 | describe '.build' do
5 | subject(:build) { factory.build(key, *args) }
6 |
7 | let(:key) { :key }
8 | let(:args) { [1, 2, 3] }
9 |
10 | before do
11 | allow(Manufacturable::Builder).to receive(:build)
12 |
13 | build
14 | end
15 |
16 | it 'delegates to the builder with the correct arguments' do
17 | expect(Manufacturable::Builder).to have_received(:build).with(Object, key, *args)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/manufacturable/dispatcher.rb:
--------------------------------------------------------------------------------
1 | module Manufacturable
2 | class Dispatcher
3 | attr_reader :message, :receiver
4 |
5 | def initialize(message:, receiver:)
6 | @message = message
7 | @receiver = receiver
8 | end
9 |
10 | def method_missing(name, *args, **kwargs)
11 | return super unless respond_to?(name)
12 |
13 | Manufacturable.build_one(receiver, name, *args, **kwargs).public_send(message)
14 | end
15 |
16 | def respond_to_missing?(name, include_private = false)
17 | Manufacturable.builds?(receiver, name) || super
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/manufacturable/config.rb:
--------------------------------------------------------------------------------
1 | module Manufacturable
2 | class Config
3 | class << self
4 | attr_writer :require_method
5 |
6 | def paths
7 | @paths ||= []
8 | end
9 |
10 | def load_paths
11 | paths.each { |path| require_path(path) }
12 | end
13 |
14 | private
15 |
16 | def require_method
17 | @require_method || :require
18 | end
19 |
20 | def require_path(path)
21 | Dir["#{path}/**/*.rb"].each { |file| require_file(file) }
22 | end
23 |
24 | def require_file(file)
25 | Kernel.public_send(require_method, file)
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/.github/workflows/ruby.yml:
--------------------------------------------------------------------------------
1 | name: Ruby
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | test:
14 |
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | ruby-version: ['3.0', '3.1', '3.2']
19 |
20 | steps:
21 | - uses: actions/checkout@v3
22 | - name: Set up Ruby
23 | uses: ruby/setup-ruby@v1
24 | with:
25 | ruby-version: ${{ matrix.ruby-version }}
26 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
27 | - name: Run tests
28 | run: bundle exec rspec
29 |
--------------------------------------------------------------------------------
/lib/manufacturable/injector.rb:
--------------------------------------------------------------------------------
1 | require 'manufacturable/simple_registrar'
2 |
3 | module Manufacturable
4 | class Injector
5 | class << self
6 | def inject(klass, *args, **kwargs)
7 | params = dependencies_for(klass).merge(**kwargs)
8 | klass.new(*args, **params)
9 | end
10 |
11 | private
12 |
13 | def dependencies_for(klass)
14 | return {} unless klass.private_method_defined?(:initialize)
15 |
16 | params = klass
17 | .instance_method(:initialize)
18 | .parameters
19 | .select { |(type, name)| type == :keyreq }
20 | .map(&:last)
21 |
22 | SimpleRegistrar.entries(*params)
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/manufacturable/simple_registrar.rb:
--------------------------------------------------------------------------------
1 | module Manufacturable
2 | class SimpleRegistrar
3 | class << self
4 | def register(key, value)
5 | self.new(registry, key).register(value)
6 | end
7 |
8 | def entries(*keys)
9 | registry.slice(*keys)
10 | end
11 |
12 | private
13 |
14 | def registry
15 | @registry ||= Hash.new
16 | end
17 | end
18 |
19 | def initialize(registry, key)
20 | @registry, @key = registry, key
21 | end
22 |
23 | def register(value)
24 | @registry[registry_key] = value
25 | end
26 |
27 | private
28 |
29 | def registry_key
30 | @registry_key ||= (@key.respond_to?(:to_sym) && @key.to_sym) || @key
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/manufacturable/factory.rb:
--------------------------------------------------------------------------------
1 | require 'manufacturable/builder'
2 |
3 | module Manufacturable
4 | module Factory
5 | def manufactures(klass)
6 | @type = klass
7 | end
8 |
9 | def build(key, *args)
10 | return if @type.nil?
11 |
12 | Builder.build(@type, key, *args)
13 | end
14 |
15 | def build_one(key, *args)
16 | return if @type.nil?
17 |
18 | Builder.build_one(@type, key, *args)
19 | end
20 |
21 | def build_many(key, *args)
22 | build_all(key, *args)
23 | end
24 |
25 | def build_all(key, *args)
26 | return [] if @type.nil?
27 |
28 | Builder.build_all(@type, key, *args)
29 | end
30 |
31 | def builds?(key)
32 | Builder.builds?(@type, key)
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/manufacturable/item.rb:
--------------------------------------------------------------------------------
1 | require 'manufacturable/registrar'
2 |
3 | module Manufacturable
4 | module Item
5 | def new(*args, **kwargs, &block)
6 | key = kwargs.delete(:manufacturable_item_key)
7 | instance = kwargs.empty? ? super(*args, &block) : super
8 | instance.instance_variable_set(:@manufacturable_item_key, key)
9 | instance
10 | end
11 |
12 | def corresponds_to(key, type = self.superclass)
13 | key = key == type ? Registrar::ALL_KEY : key
14 | Registrar.register(type, key, self)
15 | end
16 |
17 | def corresponds_to_all(type = self.superclass)
18 | corresponds_to(Registrar::ALL_KEY, type)
19 | end
20 |
21 | def default_manufacturable(type = self.superclass)
22 | corresponds_to(Registrar::DEFAULT_KEY, type)
23 | end
24 |
25 | def self.extended(base)
26 | base.include(InstanceMethods)
27 | end
28 |
29 | module InstanceMethods
30 | attr_reader :manufacturable_item_key
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/manufacturable.rb:
--------------------------------------------------------------------------------
1 | require 'manufacturable/version'
2 | require 'manufacturable/config'
3 | require 'manufacturable/factory'
4 | require 'manufacturable/item'
5 | require 'manufacturable/object_factory'
6 | require 'manufacturable/railtie'
7 | require 'manufacturable/simple_registrar'
8 | require 'manufacturable/dispatcher'
9 |
10 | module Manufacturable
11 | def self.build(*args, **kwargs, &block)
12 | Builder.build(*args, **kwargs, &block)
13 | end
14 |
15 | def self.build_one(*args, **kwargs, &block)
16 | Builder.build_one(*args, **kwargs, &block)
17 | end
18 |
19 | def self.build_many(*args, **kwargs, &block)
20 | Builder.build_all(*args, **kwargs, &block)
21 | end
22 |
23 | def self.build_all(*args, **kwargs, &block)
24 | Builder.build_all(*args, **kwargs, &block)
25 | end
26 |
27 | def self.builds?(type, key)
28 | Builder.builds?(type, key)
29 | end
30 |
31 | def self.register_dependency(key, value)
32 | SimpleRegistrar.register(key, value)
33 | end
34 |
35 | def self.reset!
36 | Registrar.reset!
37 | end
38 |
39 | def self.config
40 | yield(Config)
41 | Config.load_paths
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Alan Ridlehoover and Fito von Zastrow
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/manufacturable/registrar.rb:
--------------------------------------------------------------------------------
1 | require 'set'
2 |
3 | module Manufacturable
4 | class Registrar
5 | ALL_KEY = :__all__
6 | DEFAULT_KEY = :__default__
7 |
8 | class << self
9 | def register(type, key, value)
10 | self.new(registry, type, key).register(value)
11 | end
12 |
13 | def get(type, key)
14 | self.new(registry, type, key).get
15 | end
16 |
17 | def registered_types
18 | registry.keys
19 | end
20 |
21 | def registered_keys(type)
22 | registry[type].keys
23 | end
24 |
25 | def reset!
26 | registry.clear
27 | end
28 |
29 | private
30 |
31 | def registry
32 | @registry ||= Hash.new { |h,k| h[k] = Hash.new }
33 | end
34 | end
35 |
36 | def initialize(registry, type, key)
37 | @registry, @type, @key = registry, type, key
38 | end
39 |
40 | def register(value)
41 | assign_set if set.nil?
42 | set.add(value)
43 | end
44 |
45 | def get
46 | merged_values.empty? ? default_values : merged_values
47 | end
48 |
49 | private
50 |
51 | def registry_key
52 | @registry_key ||= (@key.respond_to?(:to_sym) && @key.to_sym) || @key
53 | end
54 |
55 | def assign_set
56 | @registry[@type][registry_key] = Set.new
57 | end
58 |
59 | def set
60 | @registry[@type][registry_key]
61 | end
62 |
63 | def merged_values
64 | key_values.merge(all_values)
65 | end
66 |
67 | def key_values
68 | get_for(registry_key)
69 | end
70 |
71 | def all_values
72 | get_for(ALL_KEY)
73 | end
74 |
75 | def default_values
76 | get_for(DEFAULT_KEY)
77 | end
78 |
79 | def get_for(key)
80 | @registry[@type][key] || Set.new
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/lib/manufacturable/builder.rb:
--------------------------------------------------------------------------------
1 | require 'manufacturable/injector'
2 | require 'manufacturable/registrar'
3 |
4 | module Manufacturable
5 | class Builder
6 | def self.build(*args, **kwargs, &block)
7 | self.new(*args, **kwargs, &block).build
8 | end
9 |
10 | def self.build_one(*args, **kwargs, &block)
11 | self.new(*args, **kwargs, &block).build_one
12 | end
13 |
14 | def self.build_all(*args, **kwargs, &block)
15 | self.new(*args, **kwargs, &block).build_all
16 | end
17 |
18 | def self.builds?(*args, **kwargs, &block)
19 | self.new(*args, **kwargs, &block).builds?
20 | end
21 |
22 | def build
23 | return_first? ? instances.first : instances
24 | end
25 |
26 | def build_one
27 | last_instance
28 | end
29 |
30 | def build_all
31 | instances
32 | end
33 |
34 | def builds?
35 | Registrar.registered_keys(type).include?(key)
36 | end
37 |
38 | private
39 |
40 | attr_reader :type, :key, :args, :kwargs, :block
41 |
42 | def initialize(*args, **kwargs, &block)
43 | @type, @key, *@args = args
44 | @kwargs, @block = kwargs, block
45 | end
46 |
47 | def return_first?
48 | instances.size < 2
49 | end
50 |
51 | def instances
52 | @instances ||= klasses.map { |klass| inject(klass) }
53 | end
54 |
55 | def last_instance
56 | inject(last_klass) unless last_klass.nil?
57 | end
58 |
59 | def inject(klass)
60 | Injector.inject(klass, *args, **kwargs_with_key).tap { |instance| block&.call(instance) }
61 | end
62 |
63 | def klasses
64 | Registrar.get(type, key).to_a
65 | end
66 |
67 | def kwargs_with_key
68 | kwargs.merge(manufacturable_item_key: key)
69 | end
70 |
71 | def last_klass
72 | klasses.last
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/spec/manufacturable/railtie_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Manufacturable::Railtie do
2 | let!(:rails_railtie) do
3 | module Rails
4 | class Railtie
5 | def self.initializer(param, &block)
6 | end
7 | end
8 | end
9 | end
10 |
11 | describe '.load' do
12 | subject(:load) { described_class.load }
13 |
14 | before do
15 | allow(described_class).to receive(:rails_defined?).and_return(rails_defined?)
16 | end
17 |
18 | context 'when Rails is NOT defined' do
19 | let(:rails_defined?) { false }
20 |
21 | before { load }
22 |
23 | it 'does not define a railtie' do
24 | expect(
25 | ObjectSpace.each_object(Class).select { |klass| klass < Rails::Railtie }
26 | ).to be_empty
27 | end
28 | end
29 |
30 | context 'when Rails is defined' do
31 | let(:rails_defined?) { true }
32 |
33 | let(:app) { instance_double('app', config: config) }
34 | let(:config) { instance_double('config', eager_load: eager_load?) }
35 |
36 | before do
37 | allow(Rails::Railtie).to receive(:initializer).and_yield(app)
38 | allow(Manufacturable::Config).to receive(:require_method=)
39 |
40 | load
41 | end
42 |
43 | context 'when the rails app does NOT use eager loading' do
44 | let(:eager_load?) { false }
45 |
46 | it 'sets require method to require' do
47 | expect(Manufacturable::Config).to have_received(:require_method=).with(:require_dependency)
48 | end
49 | end
50 |
51 | context 'when the rails app uses eager loading' do
52 | let(:eager_load?) { true }
53 |
54 | it 'sets require method to require' do
55 | expect(Manufacturable::Config).to have_received(:require_method=).with(:require)
56 | end
57 | end
58 | end
59 | end
60 | end
--------------------------------------------------------------------------------
/manufacturable.gemspec:
--------------------------------------------------------------------------------
1 | require_relative './lib/manufacturable/version'
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "manufacturable"
5 | spec.version = Manufacturable::VERSION
6 | spec.authors = ["Alan Ridlehoover", "Fito von Zastrow"]
7 | spec.email = ["administators@firsttry.software"]
8 |
9 | spec.summary = %q{Manufacturable is a factory that builds self-registering objects.}
10 | spec.description = %q{Manufacturable is a factory that builds self-registering objects. It leverages self-registration to move factory setup from case statements, hashes, and configuration files to a simple DSL within the instantiable classes themselves. Giving classes the responsibility of registering themselves with the factory does two things. It allows the factory to be extended without modification. And, it leaves the factory with only one responsibility: building objects.}
11 | spec.homepage = "https://github.com/first-try-software/manufacturable"
12 | spec.license = "MIT"
13 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14 |
15 | spec.metadata = {
16 | "bug_tracker_uri" => "https://github.com/first-try-software/manufacturable/issues",
17 | "homepage_uri" => spec.homepage,
18 | "source_code_uri" => "https://github.com/first-try-software/manufacturable"
19 | }
20 |
21 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|assets)/}) }
23 | end
24 |
25 | spec.bindir = "exe"
26 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27 | spec.require_paths = ["lib"]
28 |
29 | spec.add_development_dependency "bundler", "~> 2.0"
30 | spec.add_development_dependency "rake", "~> 12.0"
31 | spec.add_development_dependency "rspec", "~> 3.0"
32 | spec.add_development_dependency "rspec_junit_formatter", "~>0.4"
33 | spec.add_development_dependency "simplecov", "~>0.17.0"
34 | end
35 |
--------------------------------------------------------------------------------
/spec/manufacturable/config_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Manufacturable::Config do
2 | after { described_class.paths.clear }
3 |
4 | describe '.paths' do
5 | subject(:paths) { described_class.paths }
6 |
7 | context 'when there are NO manufacturable paths' do
8 | it 'returns an empty array' do
9 | expect(paths).to eq([])
10 | end
11 | end
12 |
13 | context 'when there are manufacturable paths' do
14 | let(:path1) { 'path1' }
15 | let(:path2) { 'path2' }
16 |
17 | before do
18 | paths << path1
19 | paths << path2
20 | end
21 |
22 | it 'returns an array containing the manufacturable paths' do
23 | expect(paths).to eq([path1, path2])
24 | end
25 | end
26 | end
27 |
28 | describe '.load_paths' do
29 | subject(:load_paths) { described_class.load_paths }
30 |
31 | let(:path1_files) { [file1, file2] }
32 | let(:path2_files) { [file3, file4] }
33 | let(:file1) { 'file1' }
34 | let(:file2) { 'file2' }
35 | let(:file3) { 'file3' }
36 | let(:file4) { 'file4' }
37 | let(:path1) { 'path1' }
38 | let(:path2) { 'path2' }
39 |
40 | before do
41 | described_class.paths << path1
42 | described_class.paths << path2
43 |
44 | allow(Dir).to receive(:[]).with("#{path1}/**/*.rb").and_return(path1_files)
45 | allow(Dir).to receive(:[]).with("#{path2}/**/*.rb").and_return(path2_files)
46 | described_class.require_method = require_method
47 | end
48 |
49 | context 'when a require method is NOT set' do
50 | let(:require_method) { nil }
51 |
52 | before do
53 | allow(Kernel).to receive(:require)
54 |
55 | load_paths
56 | end
57 |
58 | it 'requires all files under all manufacturable paths' do
59 | [file1, file2, file3, file4].each { |file| expect(Kernel).to have_received(:require).with(file) }
60 | end
61 | end
62 |
63 | context 'when a require method is set' do
64 | let(:require_method) { :require_dependency }
65 |
66 | before do
67 | allow(Kernel).to receive(require_method)
68 |
69 | load_paths
70 | end
71 |
72 | it 'requires all files under all manufacturable paths' do
73 | [file1, file2, file3, file4].each { |file| expect(Kernel).to have_received(require_method).with(file) }
74 | end
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/spec/manufacturable_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Manufacturable do
2 | it "has a version number" do
3 | expect(Manufacturable::VERSION).not_to be nil
4 | end
5 |
6 | describe '.build' do
7 | subject(:build) { described_class.build(args) }
8 |
9 | let(:args) { 'args' }
10 |
11 | before do
12 | allow(Manufacturable::Builder).to receive(:build)
13 |
14 | build
15 | end
16 |
17 | it 'delegates to the Builder' do
18 | expect(Manufacturable::Builder).to have_received(:build).with(args)
19 | end
20 | end
21 |
22 | describe '.build_one' do
23 | subject(:build_one) { described_class.build_one(args) }
24 |
25 | let(:args) { 'args' }
26 |
27 | before do
28 | allow(Manufacturable::Builder).to receive(:build_one)
29 |
30 | build_one
31 | end
32 |
33 | it 'delegates to the Builder' do
34 | expect(Manufacturable::Builder).to have_received(:build_one).with(args)
35 | end
36 | end
37 |
38 | describe '.build_many' do
39 | subject(:build_many) { described_class.build_many(args) }
40 |
41 | let(:args) { 'args' }
42 |
43 | before do
44 | allow(Manufacturable::Builder).to receive(:build_all)
45 |
46 | build_many
47 | end
48 |
49 | it 'delegates to the Builder' do
50 | expect(Manufacturable::Builder).to have_received(:build_all).with(args)
51 | end
52 | end
53 |
54 | describe '.build_all' do
55 | subject(:build_all) { described_class.build_all(args) }
56 |
57 | let(:args) { 'args' }
58 |
59 | before do
60 | allow(Manufacturable::Builder).to receive(:build_all)
61 |
62 | build_all
63 | end
64 |
65 | it 'delegates to the Builder' do
66 | expect(Manufacturable::Builder).to have_received(:build_all).with(args)
67 | end
68 | end
69 |
70 | describe '.builds?' do
71 | subject(:builds?) { described_class.builds?(type, key) }
72 |
73 | let(:type) { :type }
74 | let(:key) { :key }
75 |
76 | before do
77 | allow(Manufacturable::Builder).to receive(:builds?)
78 |
79 | builds?
80 | end
81 |
82 | it 'delegates to the Builder' do
83 | expect(Manufacturable::Builder).to have_received(:builds?).with(type, key)
84 | end
85 | end
86 |
87 | describe '.reset!' do
88 | subject(:reset!) { described_class.reset! }
89 |
90 | before do
91 | allow(Manufacturable::Registrar).to receive(:reset!)
92 |
93 | reset!
94 | end
95 |
96 | it 'delegates to the Registrar' do
97 | expect(Manufacturable::Registrar).to have_received(:reset!)
98 | end
99 | end
100 |
101 | describe '.config' do
102 | subject(:config) { described_class.config(&block) }
103 |
104 | let(:block) { Proc.new {} }
105 |
106 | before do
107 | allow(Manufacturable::Config).to receive(:load_paths)
108 | end
109 |
110 | it 'calls the block with the config class' do
111 | described_class.config { |arg| expect(arg).to eq(Manufacturable::Config) }
112 | end
113 |
114 | it 'loads the configured paths' do
115 | config
116 |
117 | expect(Manufacturable::Config).to have_received(:load_paths)
118 | end
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/spec/manufacturable/registrar_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Manufacturable::Registrar do
2 | subject(:registrar) { described_class }
3 |
4 | let(:type) { :type }
5 | let(:key) { :key }
6 | let(:klass) { class_double('Klass') }
7 |
8 | before { registrar.reset! }
9 | after { registrar.reset! }
10 |
11 | describe '.register' do
12 | subject(:register) { registrar.register(type, key, klass) }
13 |
14 | before { register }
15 |
16 | it 'adds the class to the registry under the type and key' do
17 | expect(registrar.get(type, key)).to include(klass)
18 | end
19 | end
20 |
21 | describe '.get' do
22 | subject(:get) { registrar.get(type, key) }
23 |
24 | context 'when the type does NOT exist in the registry' do
25 | it 'returns an empty set' do
26 | expect(get).to be_a_kind_of(Set).and(be_empty)
27 | end
28 | end
29 |
30 | context 'when the type exists in the registry' do
31 | context 'and the set corresponding to the type and key is empty' do
32 | before { registrar.register(type, :key2, klass) }
33 |
34 | context 'and there is NOT a default set' do
35 | it 'returns an empty set' do
36 | expect(get).to be_empty
37 | end
38 | end
39 |
40 | context 'and there is a default set' do
41 | let(:default_klass) { class_double('DefaultKlass') }
42 |
43 | before { registrar.register(type, Manufacturable::Registrar::DEFAULT_KEY, default_klass) }
44 |
45 | it 'returns the default set' do
46 | expect(get).to include(default_klass)
47 | end
48 | end
49 | end
50 |
51 | context 'and the set corresponding to the type and key is NOT empty' do
52 | let(:all_klass) { class_double('AllKlass') }
53 |
54 | before do
55 | registrar.register(type, Manufacturable::Registrar::ALL_KEY, all_klass)
56 | registrar.register(type, key, klass)
57 | end
58 |
59 | it 'returns a set containing all types registered for that type' do
60 | expect(get).to include(all_klass)
61 | end
62 |
63 | it 'returns a set containing all types registered for that key' do
64 | expect(get).to include(klass)
65 | end
66 | end
67 | end
68 | end
69 |
70 | describe '.registered_types' do
71 | subject(:registered_types) { registrar.registered_types }
72 |
73 | before { registrar.register(type, key, klass) }
74 |
75 | it 'returns the registered types' do
76 | expect(registered_types).to eq([type])
77 | end
78 | end
79 |
80 | describe '.registered_keys' do
81 | subject(:registered_keys) { registrar.registered_keys(type) }
82 |
83 | before { registrar.register(type, key, klass) }
84 |
85 | it 'returns the registered keys for the type' do
86 | expect(registered_keys).to eq([key])
87 | end
88 | end
89 |
90 | describe '.reset!' do
91 | subject(:reset!) { registrar.reset! }
92 |
93 | before do
94 | registrar.register(type, key, klass)
95 | end
96 |
97 | it 'clears the registry' do
98 | expect { reset! }
99 | .to change { registrar.get(type, key) }
100 | .from(including(klass))
101 | .to(be_empty)
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/spec/manufacturable/factory_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Manufacturable::Factory do
2 | subject(:factory) { Class.new { extend Manufacturable::Factory } }
3 |
4 | let(:klass) { class_double('Klass') }
5 | let(:key) { 'key' }
6 | let(:args) { 'args' }
7 |
8 | describe '.manufactures' do
9 | subject(:manufactures) { factory.manufactures(klass) }
10 |
11 | before { manufactures }
12 |
13 | it 'remembers what type of factory it is' do
14 | expect(factory.instance_variable_get(:@type)).to eq(klass)
15 | end
16 | end
17 |
18 | describe '.build' do
19 | subject(:build) { factory.build(key, *args) }
20 |
21 | context 'when manufactures has NOT been called' do
22 | it { should be_nil }
23 | end
24 |
25 | context 'when manufactures has been called' do
26 | before do
27 | allow(Manufacturable::Builder).to receive(:build)
28 | factory.manufactures(klass)
29 | build
30 | end
31 |
32 | it 'delegates to the builder' do
33 | expect(Manufacturable::Builder).to have_received(:build).with(klass, key, args)
34 | end
35 | end
36 | end
37 |
38 | describe '.build_one' do
39 | subject(:build_one) { factory.build_one(key, *args) }
40 |
41 | context 'when manufactures has NOT been called' do
42 | it { should be_nil }
43 | end
44 |
45 | context 'when manufactures has been called' do
46 | before do
47 | allow(Manufacturable::Builder).to receive(:build_one)
48 | factory.manufactures(klass)
49 | build_one
50 | end
51 |
52 | it 'delegates to the builder' do
53 | expect(Manufacturable::Builder).to have_received(:build_one).with(klass, key, args)
54 | end
55 | end
56 | end
57 |
58 | describe '.build_many' do
59 | subject(:build_many) { factory.build_many(key, *args) }
60 |
61 | context 'when manufactures has NOT been called' do
62 | it { should be_empty }
63 | end
64 |
65 | context 'when manufactures has been called' do
66 | before do
67 | allow(Manufacturable::Builder).to receive(:build_all)
68 | factory.manufactures(klass)
69 | build_many
70 | end
71 |
72 | it 'delegates to the builder' do
73 | expect(Manufacturable::Builder).to have_received(:build_all).with(klass, key, args)
74 | end
75 | end
76 | end
77 |
78 | describe '.build_all' do
79 | subject(:build_all) { factory.build_all(key, *args) }
80 |
81 | context 'when manufactures has NOT been called' do
82 | it { should be_empty }
83 | end
84 |
85 | context 'when manufactures has been called' do
86 | before do
87 | allow(Manufacturable::Builder).to receive(:build_all)
88 | factory.manufactures(klass)
89 | build_all
90 | end
91 |
92 | it 'delegates to the builder' do
93 | expect(Manufacturable::Builder).to have_received(:build_all).with(klass, key, args)
94 | end
95 | end
96 | end
97 |
98 | describe '.builds?' do
99 | subject(:builds?) { factory.builds?(key) }
100 |
101 | before do
102 | allow(Manufacturable::Builder).to receive(:builds?)
103 | factory.manufactures(klass)
104 | builds?
105 | end
106 |
107 | it 'delegates to the builder' do
108 | expect(Manufacturable::Builder).to have_received(:builds?).with(klass, key)
109 | end
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at support@firsttry.software. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: https://contributor-covenant.org
74 | [version]: https://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/spec/integration/manufacturable_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe 'Manufacturable' do
2 | describe 'trying to build a class before registering anything' do
3 | it 'returns nil' do
4 | expect(Manufacturable.build(Object, :anything)).to be_nil
5 | end
6 | end
7 |
8 | describe 'building a class registered on the Object namespace' do
9 | let!(:sedan) { Class.new { extend Manufacturable::Item; corresponds_to :four_door } }
10 |
11 | it 'returns an instance of Sedan' do
12 | expect(Manufacturable.build(Object, :four_door)).to be_a_kind_of(sedan)
13 | end
14 | end
15 |
16 | describe 'building a class registered on the namespace of an explicit superclass' do
17 | let(:automobile) { Class.new { extend Manufacturable::Item } }
18 | let!(:sedan) { Class.new(automobile) { corresponds_to :four_door } }
19 |
20 | it 'returns an instance of Sedan' do
21 | expect(Manufacturable.build(automobile, :four_door)).to be_a_kind_of(sedan)
22 | end
23 | end
24 |
25 | describe 'building a default manufacturable' do
26 | let(:automobile) { Class.new { extend Manufacturable::Item } }
27 | let!(:sedan) { Class.new(automobile) { default_manufacturable } }
28 |
29 | it 'returns an instance of Sedan' do
30 | expect(Manufacturable.build(automobile, :unknown_key)).to be_a_kind_of(sedan)
31 | end
32 | end
33 |
34 | describe 'building a class with named params' do
35 | let(:automobile) { Class.new { extend Manufacturable::Item; def initialize(color:); end } }
36 | let!(:sedan) { Class.new(automobile) { corresponds_to :sedan } }
37 |
38 | around do |example|
39 | original_stderror = $stderr
40 | $stderr = StringIO.new
41 | example.run
42 | $stderr = original_stderror
43 | end
44 |
45 | it 'does not warn' do
46 | Manufacturable.build_one(automobile, :sedan, color: 'blue')
47 |
48 | expect($stderr.string).to be_empty
49 | end
50 | end
51 |
52 | describe 'building multiple classes registered with the same key' do
53 | let(:component) { Class.new { extend Manufacturable::Item } }
54 | let!(:engine) { Class.new(component) { corresponds_to :sedan } }
55 | let!(:transmission) { Class.new(component) { corresponds_to :sedan } }
56 |
57 | it 'returns an instance of Sedan' do
58 | expect(Manufacturable.build(component, :sedan))
59 | .to match_array([a_kind_of(engine), a_kind_of(transmission)])
60 | end
61 | end
62 |
63 | describe 'building a class that corresponds to all keys in a namespace' do
64 | let(:component) { Class.new { extend Manufacturable::Item } }
65 | let!(:standard_engine) { Class.new(component) { corresponds_to :sedan } }
66 | let!(:performance_engine) { Class.new(component) { corresponds_to :coupe } }
67 | let!(:transmission) { Class.new(component) { corresponds_to_all } }
68 |
69 | it 'returns an instance of Sedan' do
70 | expect(Manufacturable.build(component, :sedan))
71 | .to match_array([a_kind_of(standard_engine), a_kind_of(transmission)])
72 | expect(Manufacturable.build(component, :coupe))
73 | .to match_array([a_kind_of(performance_engine), a_kind_of(transmission)])
74 | end
75 | end
76 |
77 | describe 'injecting a registered dependency' do
78 | let(:car) { Class.new { extend Manufacturable::Item } }
79 | let(:driver) { instance_double('driver', name: 'Müller') }
80 | let!(:bmw_m3) do
81 | Class.new(car) do
82 | corresponds_to :sedan
83 |
84 | attr_reader :driver
85 |
86 | def initialize(driver:)
87 | @driver = driver
88 | end
89 | end
90 | end
91 |
92 | it 'builds an instance of sedan with the dependency' do
93 | Manufacturable.register_dependency(:driver, driver)
94 |
95 | expect(Manufacturable.build(car, :sedan)).to have_attributes(driver: driver)
96 | end
97 | end
98 |
99 | describe 'dispatching a message to a receiver' do
100 | let(:action) { Class.new { extend Manufacturable::Item; def initialize(observer); @observer = observer; end } }
101 | let!(:check_engine) { Class.new(action) { corresponds_to :check_engine; def perform; @observer.report(:engine_checked); end } }
102 | let!(:change_oil) { Class.new(action) { corresponds_to :change_oil; def perform; @observer.report(:oil_changed); end } }
103 |
104 | it 'builds an instance and dispatches the message to it' do
105 | observer = instance_double('observer', report: true)
106 | dispatcher = Manufacturable::Dispatcher.new(receiver: action, message: :perform)
107 |
108 | dispatcher.check_engine(observer)
109 | dispatcher.change_oil(observer)
110 |
111 | expect(observer).to have_received(:report).with(:engine_checked)
112 | expect(observer).to have_received(:report).with(:oil_changed)
113 | end
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/spec/manufacturable/item_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Manufacturable::Item do
2 | let(:item) { Class.new(base_klass) { extend Manufacturable::Item } }
3 | let(:base_klass) { Class.new }
4 | let(:type) { base_klass }
5 |
6 | before do
7 | allow(Manufacturable::Registrar).to receive(:register)
8 | end
9 |
10 | describe '.corresponds_to' do
11 | subject(:corresponds_to) { item.corresponds_to(*args) }
12 |
13 | let(:args) { [key] }
14 |
15 | before { corresponds_to }
16 |
17 | context 'when the type is NOT provided' do
18 | context 'when the key does NOT match the item type' do
19 | let(:key) { :key }
20 |
21 | it 'registers itself with the key' do
22 | expect(Manufacturable::Registrar).to have_received(:register).with(type, key, item)
23 | end
24 | end
25 |
26 | context 'when the key matches the item type' do
27 | let(:key) { type }
28 |
29 | it 'registers itself for all items of that type' do
30 | expect(Manufacturable::Registrar).to have_received(:register).with(type, Manufacturable::Registrar::ALL_KEY, item)
31 | end
32 | end
33 | end
34 |
35 | context 'when the type is provided' do
36 | let(:args) { [key, explicit_type] }
37 | let(:key) { :key }
38 | let(:explicit_type) { :explicit_type }
39 |
40 | it 'registers itself with the key and type' do
41 | expect(Manufacturable::Registrar).to have_received(:register).with(explicit_type, key, item)
42 | end
43 | end
44 | end
45 |
46 | describe '.corresponds_to_all' do
47 | subject(:corresponds_to_all) { item.corresponds_to_all(*args) }
48 |
49 | let(:args) { [] }
50 |
51 | before { corresponds_to_all }
52 |
53 | it 'registers itself for all items of that type' do
54 | expect(Manufacturable::Registrar).to have_received(:register).with(type, Manufacturable::Registrar::ALL_KEY, item)
55 | end
56 |
57 | context 'when the type is provided' do
58 | let(:args) { [explicit_type] }
59 | let(:explicit_type) { :explicit_type }
60 |
61 | it 'registers itself for all items of the specified type' do
62 | expect(Manufacturable::Registrar).to have_received(:register).with(explicit_type, Manufacturable::Registrar::ALL_KEY, item)
63 | end
64 | end
65 | end
66 |
67 | describe '.default_manufacturable' do
68 | subject(:default_manufacturable) { item.default_manufacturable(*args) }
69 |
70 | let(:args) { [] }
71 |
72 | before { default_manufacturable }
73 |
74 | it 'registers itself as the default item for that type' do
75 | expect(Manufacturable::Registrar).to have_received(:register).with(type, Manufacturable::Registrar::DEFAULT_KEY, item)
76 | end
77 |
78 | context 'when the type is provided' do
79 | let(:args) { [explicit_type] }
80 | let(:explicit_type) { :explicit_type }
81 |
82 | it 'registers itself as the default item for the specified type' do
83 | expect(Manufacturable::Registrar).to have_received(:register).with(explicit_type, Manufacturable::Registrar::DEFAULT_KEY, item)
84 | end
85 | end
86 | end
87 |
88 | describe '.new' do
89 | subject { item.new(*args, **kwargs) }
90 |
91 | let(:args) { [] }
92 | let(:kwargs) { {} }
93 |
94 | context 'when the extending class does NOT define an initializer' do
95 | let(:item) { Class.new { extend Manufacturable::Item } }
96 |
97 | context 'and a manufacturable_item_key argument is NOT passed' do
98 |
99 | it 'sets manufacturable_item_key to nil' do
100 | expect(subject.manufacturable_item_key).to be_nil
101 | end
102 | end
103 |
104 | context 'and a manufacturable_item_key argument is passed' do
105 | let(:kwargs) { { manufacturable_item_key: key } }
106 | let(:key) { 'key' }
107 |
108 | it 'sets manufacturable_item_key to the provided value' do
109 | expect(subject.manufacturable_item_key).to eq(key)
110 | end
111 | end
112 | end
113 |
114 | context 'when the extending class defines an initializer with positional arguments' do
115 | let(:item) { Class.new { extend Manufacturable::Item; def initialize(foo); @foo = foo; end } }
116 |
117 | context 'and a manufacturable_item_key argument is NOT passed' do
118 | let(:args) { [:bar] }
119 | let(:kwargs) { {} }
120 |
121 | it 'sets manufacturable_item_key to nil' do
122 | expect(subject.manufacturable_item_key).to be_nil
123 | end
124 | end
125 |
126 | context 'and a manufacturable_item_key argument is passed' do
127 | let(:args) { [:bar] }
128 | let(:kwargs) { { manufacturable_item_key: key } }
129 | let(:key) { 'key' }
130 |
131 | it 'sets manufacturable_item_key to the provided value' do
132 | expect(subject.manufacturable_item_key).to eq(key)
133 | end
134 | end
135 | end
136 |
137 | context 'when the extending class defines an initializer with named arguments' do
138 | let(:item) { Class.new { extend Manufacturable::Item; def initialize(foo:); @foo = foo; end } }
139 |
140 | context 'and a manufacturable_item_key argument is NOT passed' do
141 | let(:args) { [] }
142 | let(:kwargs) { {foo: :bar} }
143 |
144 | it 'sets manufacturable_item_key to nil' do
145 | expect(subject.manufacturable_item_key).to be_nil
146 | end
147 | end
148 |
149 | context 'and a manufacturable_item_key argument is passed' do
150 | let(:kwargs) { { foo: :bar, manufacturable_item_key: key } }
151 | let(:key) { 'key' }
152 |
153 | it 'sets manufacturable_item_key to the provided value' do
154 | expect(subject.manufacturable_item_key).to eq(key)
155 | end
156 | end
157 | end
158 | end
159 | end
160 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Manufacturable
4 |
5 | [](https://badge.fury.io/rb/manufacturable)
6 | [
7 | [](https://codeclimate.com/github/first-try-software/manufacturable/maintainability)
8 | [](https://codeclimate.com/github/first-try-software/manufacturable/test_coverage)
9 |
10 | Manufacturable is a factory that builds self-registering objects.
11 |
12 | It leverages self-registration to move factory setup from case statements,
13 | hashes, and configuration files to a simple DSL within the instantiable
14 | classes themselves. Giving classes the responsibility of registering
15 | themselves with the factory does two things. It allows the factory to be
16 | [extended without modification][ocp]. And, it leaves the factory with only
17 | [one responsibility][srp]: building objects.
18 |
19 | ## Motivation
20 |
21 | We wrote Manufacturable, so we wouldn't have to keep modifying our factory
22 | code every time we needed to add functionality to our applications. For
23 | example, consider this factory:
24 |
25 | ```ruby
26 | class AutomobileFactory
27 | def self.build(type, *args)
28 | case type
29 | when :sedan
30 | Sedan.new(*args)
31 | when :coupe
32 | Coupe.new(*args)
33 | when :convertible
34 | Convertible.new(*args)
35 | end
36 | end
37 | end
38 | ```
39 |
40 | If you want to start building `Hatchback` objects, you'll need to modify the
41 | factory. To solve this problem in Ruby, factories are often built using
42 | metaprogramming, like this:
43 |
44 | ```ruby
45 | class AutomobileFactory
46 | def self.build(type, *args)
47 | Object.const_get(type.capitalize)&.new(*args)
48 | end
49 | end
50 | ```
51 |
52 | But, this very simple factory relies on a convention: the type symbol must
53 | match the name of the class. This means that classes with namespaces, or
54 | symbols with underscores will not work. In other words, you could not use the
55 | symbol `:four_door` to build a `Sedan` object.
56 |
57 | Manufacturable solves these problems by allowing classes to register themselves
58 | with the factory using a key of their choosing. This means you never have to
59 | modify the factory code again.
60 |
61 | ## Usage
62 |
63 | ### The Basics
64 |
65 | A class may register itself with Manufacturable like this:
66 |
67 | ```ruby
68 | class Sedan
69 | extend Manufacturable::Item
70 |
71 | corresponds_to :four_door
72 | end
73 | ```
74 |
75 | Extending `Manufacturable::Item` adds the Manufacturable DSL to the class.
76 | Calling `corresponds_to` with a key registers that class with the factory.
77 |
78 | Once registered, a class may be instantiated like this:
79 |
80 | ```ruby
81 | Manufacturable.build(Object, :four_door, *args)
82 | ```
83 |
84 | Note the first parameter. This is the parent class of the registered class.
85 | In this case, the parent class happens to be `Object`. So, Manufacturable
86 | registered the `Sedan` class under the `Object` namespace to prevent key
87 | collision. To instantiate the `Sedan`, we need to request the `:four_door`
88 | key from the `Object` namespace.
89 |
90 | For convenience, Manufacturable provides an `ObjectFactory` to build objects
91 | that are stored in the `Object` namespace:
92 |
93 | ```ruby
94 | Manufacturable::ObjectFactory.build(:four_door, *args)
95 | ```
96 |
97 | In most cases, though, your class will actually inherit from a specific class
98 | other than `Object`. For example, it is likely that the `Sedan` class would
99 | inherit from an `Automobile` class. If that were the case, you would pass
100 | `Automobile` as the first parameter to `Manufacturable.build`:
101 |
102 | ```ruby
103 | class Automobile
104 | extend Manufacturable::Item
105 | end
106 |
107 | class Sedan < Automobile
108 | corresponds_to :four_door
109 | end
110 |
111 | Manufacturable.build(Automobile, :four_door, *args)
112 | ```
113 |
114 | That's all you need to know to begin using Manufacturable. But, it's not all
115 | there is to know. Manunfacturable allows you to:
116 |
117 | * [Configure your own factory classes](#using-factory-classes)
118 | * [Define a default manufacturable item](#defining-a-default-manufacturable-item)
119 | * [Register multiple classes under the same key within a namespace](#registering-multiple-classes-under-the-same-key-within-a-namespace)
120 | * [Register a class to correspond with an entire namespace](#registering-a-class-to-correspond-with-an-entire-namespace)
121 |
122 | ### Using Factory Classes
123 |
124 | Manufacturable also has a DSL for creating factories:
125 |
126 | ```ruby
127 | class AutomobileFactory
128 | extend Manufacturable::Factory
129 |
130 | manufactures Automobile
131 | end
132 | ```
133 |
134 | Extending `Manufacturable::Factory` adds the DSL to the factory class.
135 | Calling `manufactures` with a class designates it as the namespace for the
136 | factory.
137 |
138 | Once configured, you can use the `AutomobileFactory` to build objects from
139 | classes in the `Automobile` namespace:
140 |
141 | ```ruby
142 | AutomobileFactory.build(:four_door, *args)
143 | ```
144 |
145 | ### Defining a Default Manufacturable Item
146 |
147 | What happens when Manufacturable is unable to find the key you're looking for?
148 | That depends on what you tell Manufacturable. By default, it will return `nil`
149 | when it does not find a class registered at a specific key. But, you can also
150 | configure Manufacturable's response. This allows you to implement the [null
151 | object pattern][nop].
152 |
153 | ```ruby
154 | class NullAutomobile < Automobile
155 | default_manufacturable
156 | end
157 | ```
158 |
159 | Now, your calling code does not have to check for `nil` before calling a method
160 | on the class:
161 |
162 | ```ruby
163 | AutomobileFactory.build(:lemon, *args).drive
164 | ```
165 |
166 | ### Registering Multiple Classes Under the Same Key within a Namespace
167 |
168 | Manufacturable allows you to register multiple classes under the same key:
169 |
170 | ```ruby
171 | class StandardEngine < Component
172 | corresponds_to :sedan
173 | end
174 |
175 | class AutomaticTransmission < Component
176 | corresponds_to :sedan
177 | end
178 |
179 | class PowerfulEngine < Component
180 | corresponds_to :coupe
181 | end
182 |
183 | class ManualTransmission < Component
184 | corresponds_to :coupe
185 | end
186 | ```
187 |
188 | Then, when you request that key, you'll receive an array containing a new
189 | instance of each class registered under that key.
190 |
191 | ```ruby
192 | ComponentFactory.build(:sedan, *args)
193 | # => [#, #]
194 |
195 | ComponentFactory.build(:coupe, *args)
196 | # => [#, #]
197 | ```
198 |
199 | ### Registering a Class to Correspond with an Entire Namespace
200 |
201 | Manufacturable will also let you register a class that corresponds with all
202 | of the keys in a namespace:
203 |
204 | ```ruby
205 | class HeadLight < Component
206 | corresponds_to_all
207 | end
208 | ```
209 |
210 | Now, the `ComponentFactory` will include `HeadLight` objects for both the
211 | `:sedan` and `:coupe`.
212 |
213 | ```ruby
214 | ComponentFactory.build(:sedan, *args)
215 | # => [
216 | # #,
217 | # #,
218 | # #
219 | # ]
220 |
221 | ComponentFactory.build(:coupe, *args)
222 | # => [
223 | # #,
224 | # #,
225 | # #
226 | # ]
227 | ```
228 |
229 | ## Installation
230 |
231 | Add this line to your application's Gemfile:
232 |
233 | ```ruby
234 | gem 'manufacturable'
235 | ```
236 |
237 | And then execute:
238 |
239 | $ bundle install
240 |
241 | Or install it yourself as:
242 |
243 | $ gem install manufacturable
244 |
245 | If you are using Manufacturable with Rails, you'll need an initializer to tell
246 | manufacturable where the classes are, so they can be autoloaded.
247 |
248 | ```ruby
249 | Manufacturable.config do |config|
250 | config.paths << Rails.root.join('app', 'automobiles')
251 | config.paths << Rails.root.join('app', 'components')
252 | end
253 | ```
254 |
255 | ## Development
256 |
257 | After checking out the repo, run `bin/setup` to install dependencies. Then,
258 | run `rake spec` to run the tests. You can also run `bin/console` for an
259 | interactive prompt that will allow you to experiment. To install this gem
260 | onto your local machine, run `bundle exec rake install`.
261 |
262 | ## Contributing
263 |
264 | Bug reports and pull requests are welcome on [GitHub][git].
265 |
266 | ## License
267 |
268 | The gem is available as open source under the terms of the [MIT License][mit].
269 |
270 | ## Code of Conduct
271 |
272 | Everyone interacting in the Manufacturable project's codebases, issue trackers,
273 | chat rooms and mailing lists is expected to follow the [code of conduct][cod].
274 |
275 | ## Acknowledgements
276 |
277 | Manufacturable was inspired by work we did at Entelo on [Industrialist][ind].
278 | We will be forever grateful to the people at Entelo for giving us the
279 | opportunity to work on things we're still proud of today.
280 |
281 | [srp]: https://en.wikipedia.org/wiki/Single-responsibility_principle
282 | [ocp]: https://en.wikipedia.org/wiki/Open-closed_principle
283 | [nop]: https://en.wikipedia.org/wiki/Null_object_pattern
284 | [gem]: https://rubygems.org
285 | [git]: https://github.com/first-try-software/manufacturable
286 | [cod]: https://github.com/first-try-software/manufacturable/blob/main/CODE_OF_CONDUCT.md
287 | [mit]: https://opensource.org/licenses/MIT
288 | [ind]: https://github.com/entelo/industrialist
289 |
--------------------------------------------------------------------------------
/spec/manufacturable/builder_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Manufacturable::Builder do
2 | let(:type) { 'type' }
3 | let(:key) { 'key' }
4 | let(:args) { 'args' }
5 | let(:kwargs) { { manufacturable_item_key: key } }
6 | let(:klasses) { Set.new }
7 |
8 | describe '.build' do
9 | subject(:build) { described_class.build(type, key, args) }
10 |
11 | before do
12 | allow(Manufacturable::Registrar).to receive(:get).and_return(klasses)
13 | end
14 |
15 | it 'gets the classes from the Registrar' do
16 | build
17 |
18 | expect(Manufacturable::Registrar).to have_received(:get).with(type, key)
19 | end
20 |
21 | context 'when the class set is empty' do
22 | let(:klasses) { Set.new }
23 |
24 | it { should be_nil }
25 | end
26 |
27 | context 'when the class set is NOT empty' do
28 | let(:klass) { class_double('Klass', new: klass_instance) }
29 | let(:klass_instance) { instance_double('Klass') }
30 | let(:klasses) { Set.new([klass]) }
31 |
32 | context 'and the set contains one class' do
33 | context 'and the params are NOT named' do
34 | it 'instantiates an object with the provided args' do
35 | build
36 |
37 | expect(klass).to have_received(:new).with(args, kwargs)
38 | end
39 |
40 | it 'returns an instance of the class in the set' do
41 | expect(build).to eq(klass_instance)
42 | end
43 |
44 | context 'and a block is given' do
45 | it 'yields the built instance to the block' do
46 | expect { |b| described_class.build(type, key, args, &b) }.to yield_with_args(klass_instance)
47 | end
48 | end
49 | end
50 |
51 | context 'and the params are named' do
52 | let(:klass) { Class.new { extend Manufacturable::Item; def initialize(param:); end } }
53 |
54 | around do |example|
55 | original_stderror = $stderr
56 | $stderr = StringIO.new
57 | example.run
58 | $stderr = original_stderror
59 | end
60 |
61 | it 'does not raise a warning' do
62 | described_class.build(type, key, param: 'param')
63 |
64 | expect($stderr.string).to be_empty
65 | end
66 | end
67 | end
68 |
69 | context 'and the set contains more than one class' do
70 | let(:klass1) { class_double('Klass1', new: klass1_instance) }
71 | let(:klass2) { class_double('Klass2', new: klass2_instance) }
72 | let(:klass1_instance) { instance_double('Klass1') }
73 | let(:klass2_instance) { instance_double('Klass2') }
74 | let(:klasses) { Set.new([klass1, klass2]) }
75 |
76 | it 'instantiates an object with the provided args' do
77 | build
78 |
79 | expect(klasses).to all(have_received(:new).with(args, kwargs))
80 | end
81 |
82 | it 'returns an instance of every class in the set' do
83 | expect(build).to eq([klass1_instance, klass2_instance])
84 | end
85 |
86 | context 'and a block is given' do
87 | it 'successively yields the built instances to the block' do
88 | expect { |b| described_class.build(type, key, args, &b) }.to yield_successive_args(klass1_instance, klass2_instance)
89 | end
90 | end
91 | end
92 | end
93 | end
94 |
95 | describe '.build_one' do
96 | subject(:build_one) { described_class.build_one(type, key, args) }
97 |
98 | before do
99 | allow(Manufacturable::Registrar).to receive(:get).and_return(klasses)
100 | end
101 |
102 | it 'gets the classes from the Registrar' do
103 | build_one
104 |
105 | expect(Manufacturable::Registrar).to have_received(:get).with(type, key)
106 | end
107 |
108 | context 'when the class set is empty' do
109 | let(:klasses) { Set.new }
110 |
111 | it { should be_nil }
112 | end
113 |
114 | context 'when the class set is NOT empty' do
115 | let(:klass) { class_double('Klass', new: klass_instance) }
116 | let(:klass_instance) { instance_double('Klass') }
117 | let(:klasses) { Set.new([klass]) }
118 |
119 | context 'and the set contains one class' do
120 | context 'and the params are NOT named' do
121 | it 'instantiates an object with the provided args' do
122 | build_one
123 |
124 | expect(klass).to have_received(:new).with(args, kwargs)
125 | end
126 |
127 | it 'returns an instance of the class in the set' do
128 | expect(build_one).to eq(klass_instance)
129 | end
130 |
131 | context 'and a block is given' do
132 | it 'yields the built instance to the block' do
133 | expect { |b| described_class.build_one(type, key, args, &b) }.to yield_with_args(klass_instance)
134 | end
135 | end
136 | end
137 |
138 | context 'and the params are named' do
139 | let(:klass) { Class.new { extend Manufacturable::Item; def initialize(param:); end } }
140 |
141 | around do |example|
142 | original_stderror = $stderr
143 | $stderr = StringIO.new
144 | example.run
145 | $stderr = original_stderror
146 | end
147 |
148 | it 'does not raise a warning' do
149 | described_class.build_one(type, key, param: 'param')
150 |
151 | expect($stderr.string).to be_empty
152 | end
153 | end
154 | end
155 |
156 | context 'and the set contains more than one class' do
157 | let(:klass1) { class_double('Klass1', new: klass1_instance) }
158 | let(:klass2) { class_double('Klass2', new: klass2_instance) }
159 | let(:klass1_instance) { instance_double('Klass1') }
160 | let(:klass2_instance) { instance_double('Klass2') }
161 | let(:klasses) { Set.new([klass1, klass2]) }
162 |
163 | it 'instantiates the last class with the provided args' do
164 | build_one
165 |
166 | expect(klass1).not_to have_received(:new).with(args, kwargs)
167 | expect(klass2).to have_received(:new).with(args, kwargs)
168 | end
169 |
170 | it 'returns an instance of the last class in the set' do
171 | expect(build_one).to eq(klass2_instance)
172 | end
173 |
174 | context 'and a block is given' do
175 | it 'yields the built instance to the block' do
176 | expect { |b| described_class.build_one(type, key, args, &b) }.to yield_with_args(klass2_instance)
177 | end
178 | end
179 | end
180 | end
181 | end
182 |
183 | describe '.build_all' do
184 | subject(:build_all) { described_class.build_all(type, key, args) }
185 |
186 | before do
187 | allow(Manufacturable::Registrar).to receive(:get).and_return(klasses)
188 | end
189 |
190 | it 'gets the classes from the Registrar' do
191 | build_all
192 |
193 | expect(Manufacturable::Registrar).to have_received(:get).with(type, key)
194 | end
195 |
196 | context 'when the class set is empty' do
197 | let(:klasses) { Set.new }
198 |
199 | it { should be_empty }
200 | end
201 |
202 | context 'when the class set is NOT empty' do
203 | let(:klass) { class_double('Klass', new: klass_instance) }
204 | let(:klass_instance) { instance_double('Klass') }
205 | let(:klasses) { Set.new([klass]) }
206 |
207 | context 'and the set contains one class' do
208 | context 'and the params are NOT named' do
209 | it 'instantiates an object with the provided args' do
210 | build_all
211 |
212 | expect(klass).to have_received(:new).with(args, kwargs)
213 | end
214 |
215 | it 'returns an array containing an instance of the class in the set' do
216 | expect(build_all).to eq([klass_instance])
217 | end
218 |
219 | context 'and a block is given' do
220 | it 'yields the built instance to the block' do
221 | expect { |b| described_class.build_all(type, key, args, &b) }.to yield_with_args(klass_instance)
222 | end
223 | end
224 | end
225 |
226 | context 'and the params are named' do
227 | let(:klass) { Class.new { extend Manufacturable::Item; def initialize(param:); end } }
228 |
229 | around do |example|
230 | original_stderror = $stderr
231 | $stderr = StringIO.new
232 | example.run
233 | $stderr = original_stderror
234 | end
235 |
236 | it 'does not raise a warning' do
237 | described_class.build_all(type, key, param: 'param')
238 |
239 | expect($stderr.string).to be_empty
240 | end
241 | end
242 | end
243 |
244 | context 'and the set contains more than one class' do
245 | let(:klass1) { class_double('Klass1', new: klass1_instance) }
246 | let(:klass2) { class_double('Klass2', new: klass2_instance) }
247 | let(:klass1_instance) { instance_double('Klass1') }
248 | let(:klass2_instance) { instance_double('Klass2') }
249 | let(:klasses) { Set.new([klass1, klass2]) }
250 |
251 | it 'instantiates an object with the provided args' do
252 | build_all
253 |
254 | expect(klasses).to all(have_received(:new).with(args, kwargs))
255 | end
256 |
257 | it 'returns an instance of every class in the set' do
258 | expect(build_all).to eq([klass1_instance, klass2_instance])
259 | end
260 |
261 | context 'and a block is given' do
262 | it 'successively yields the built instances to the block' do
263 | expect { |b| described_class.build_all(type, key, args, &b) }.to yield_successive_args(klass1_instance, klass2_instance)
264 | end
265 | end
266 | end
267 | end
268 | end
269 |
270 | describe '.builds?' do
271 | subject(:builds?) { described_class.builds?(type, key) }
272 |
273 | before do
274 | allow(Manufacturable::Registrar).to receive(:registered_keys).with(type).and_return(registered_keys)
275 | end
276 |
277 | context 'when the key is registered for the type' do
278 | let(:registered_keys) { [key] }
279 |
280 | it 'returns true' do
281 | expect(builds?).to eq(true)
282 | end
283 | end
284 |
285 | context 'when the key is NOT registered for the type' do
286 | let(:registered_keys) { [:different_key] }
287 |
288 | it 'returns false' do
289 | expect(builds?).to eq(false)
290 | end
291 | end
292 | end
293 | end
294 |
--------------------------------------------------------------------------------