├── foo.txt ├── .rspec ├── lib ├── makery │ ├── version.rb │ ├── builder.rb │ └── factory.rb └── makery.rb ├── .simplecov ├── bin ├── setup └── console ├── .gitignore ├── Gemfile ├── Rakefile ├── .travis.yml ├── spec ├── spec_helper.rb └── makery_spec.rb ├── LICENSE.txt ├── makery.gemspec ├── benchmark.rb ├── .rubocop.yml ├── CODE_OF_CONDUCT.md └── README.md /foo.txt: -------------------------------------------------------------------------------- 1 | hi 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/makery/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Makery 4 | VERSION = "0.1.0" 5 | end 6 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | SimpleCov.start do 4 | load_profile "test_frameworks" 5 | minimum_coverage 100 6 | end 7 | -------------------------------------------------------------------------------- /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 | Gemfile.lock 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Specify your gem's dependencies in makery.gemspec 8 | gemspec 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | require "rubocop/rake_task" 9 | 10 | RuboCop::RakeTask.new 11 | 12 | task default: %w[spec rubocop] 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "makery" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - '2.5' 5 | - '2.6' 6 | - '2.7' 7 | - ruby-head 8 | before_install: gem install bundler -v 1.16.0 9 | install: "bundle install --jobs 8" 10 | script: 11 | - bundle exec rake 12 | - bundle exec rubocop 13 | deploy: 14 | provider: rubygems 15 | on: 16 | tags: true 17 | matrix: 18 | fast_finish: true 19 | allow_failures: 20 | - rvm: ruby-head 21 | branches: 22 | only: 23 | - master 24 | -------------------------------------------------------------------------------- /lib/makery.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "makery/version" 4 | require "makery/factory" 5 | 6 | # The main interface to the factories. 7 | module Makery 8 | def self.[](klass) 9 | makers[klass] 10 | end 11 | 12 | def self.[]=(key, callable) 13 | makers[key] = callable 14 | end 15 | 16 | def self.makers 17 | @makers ||= new_makers 18 | end 19 | 20 | def self.new_makers 21 | Hash.new { |h, k| h[k] = Factory.new(k) } 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "pry" 5 | require "simplecov" unless ENV["NO_COVERAGE"] 6 | 7 | RSpec.configure do |config| 8 | # Enable flags like --only-failures and --next-failure 9 | config.example_status_persistence_file_path = ".rspec_status" 10 | 11 | # Disable RSpec exposing methods globally on `Module` and `main` 12 | config.disable_monkey_patching! 13 | 14 | config.expect_with :rspec do |c| 15 | c.syntax = :expect 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/makery/builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Makery 4 | using(Module.new do 5 | refine(Object) { define_method(:makery_eval) { |_| self } } 6 | refine(Proc) { alias_method(:makery_eval, :call) } 7 | end) 8 | 9 | # Builder builds the instance 10 | Builder = Struct.new(:attrs, :object, :i) do 11 | def self.call(*args) 12 | new(*args).call 13 | end 14 | 15 | def call 16 | attrs.each_key { |k| evaluate_attr(k) } 17 | end 18 | 19 | def evaluate_attr(attr) 20 | object.send("#{attr}=", attrs[attr].makery_eval(self)) 21 | end 22 | alias_method :[], :evaluate_attr 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/makery/factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "makery/builder" 4 | 5 | module Makery 6 | # The factory is the stucture that stores the parts for the builder. 7 | Factory = Struct.new(:klass) do 8 | attr_accessor :count, :traits_repository 9 | def initialize(*args) 10 | self.count = 0 11 | self.traits_repository = {} 12 | super 13 | end 14 | 15 | def call(*traits, **override) 16 | attrs = base.merge(**trait_attrs(traits), **override) 17 | self.count = count + 1 18 | klass.new.tap { |obj| Builder.call(attrs, obj, count) } 19 | end 20 | 21 | def base(**attrs) 22 | @base ||= attrs 23 | end 24 | 25 | def trait_attrs(traits) 26 | traits.map { |t| traits_repository[t] }.reduce({}, &:merge) 27 | end 28 | 29 | def trait(name, **attrs) 30 | traits_repository[name] = attrs 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Kelly Wolf Stannard 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 | -------------------------------------------------------------------------------- /makery.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "makery/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "makery" 9 | spec.version = Makery::VERSION 10 | spec.authors = ["Kelly Wolf Stannard"] 11 | spec.email = ["kwstannard@gmail.com"] 12 | 13 | spec.summary = "A minimalist factory gem" 14 | spec.description = "A minimalist factory gem" 15 | spec.homepage = "https://github.com/kwstannard/makery" 16 | spec.required_ruby_version = ">= 2.3" 17 | spec.license = "MIT" 18 | 19 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 20 | # 'allowed_push_host' to allow pushing to a single host or delete this 21 | # section to allow pushing to any host. 22 | if spec.respond_to?(:metadata) 23 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 24 | else 25 | raise "RubyGems 2.0 or newer is required to protect against " \ 26 | "public gem pushes." 27 | end 28 | 29 | spec.files = Dir.glob("lib/**/*.rb") 30 | spec.require_paths = ["lib"] 31 | 32 | spec.add_development_dependency "benchmark-ips" 33 | spec.add_development_dependency "bundler" 34 | spec.add_development_dependency "factory_bot" 35 | spec.add_development_dependency "pry" 36 | spec.add_development_dependency "rake" 37 | spec.add_development_dependency "rspec" 38 | spec.add_development_dependency "rubocop", "0.89.1" 39 | spec.add_development_dependency "rubocop-rspec", "1.43.1" 40 | spec.add_development_dependency "simplecov" 41 | end 42 | -------------------------------------------------------------------------------- /benchmark.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "factory_bot" 4 | require "makery" 5 | require "benchmark/ips" 6 | 7 | foo = Struct.new(:bar) 8 | delayed = Struct.new(:bar) 9 | 10 | Makery[foo].base( 11 | bar: 1 12 | ) 13 | Makery[foo].trait( 14 | :big, 15 | bar: 2 16 | ) 17 | 18 | Makery[delayed].base( 19 | bar: ->(_m) { 5 } 20 | ) 21 | 22 | FactoryBot.factories.clear 23 | FactoryBot.define do 24 | factory :foo, class: foo do 25 | bar { 1 } 26 | 27 | trait :big do 28 | bar { 2 } 29 | end 30 | end 31 | 32 | factory :delayed, class: delayed do 33 | bar { 5 } 34 | end 35 | end 36 | 37 | puts Makery[foo].call(:big) 38 | puts Makery[foo].call(bar: 2) 39 | puts Makery[foo].call(:big, bar: 3) 40 | puts Makery[delayed].call 41 | 42 | Benchmark.ips(quiet: true) do |rep| 43 | rep.report("control init + set") do 44 | foo.new.bar = 2 45 | end 46 | 47 | rep.report("makery base") do 48 | Makery[foo].call 49 | end 50 | 51 | rep.report("factory_bot base") do 52 | FactoryBot.build(:foo) 53 | end 54 | 55 | rep.compare! 56 | end 57 | 58 | Benchmark.ips(quiet: true) do |rep| 59 | rep.report("makery trait") do 60 | Makery[foo].call(:big) 61 | end 62 | 63 | rep.report("factory_bot trait") do 64 | FactoryBot.build(:foo, :big) 65 | end 66 | rep.compare! 67 | end 68 | 69 | Benchmark.ips(quiet: true) do |rep| 70 | rep.report("makery override") do 71 | Makery[foo].call(bar: 2) 72 | end 73 | 74 | rep.report("factory_bot override") do 75 | FactoryBot.build(:foo, bar: 2) 76 | end 77 | rep.compare! 78 | end 79 | 80 | Benchmark.ips(quiet: true) do |rep| 81 | rep.report("makery trait + override") do 82 | Makery[foo].call(:big, bar: 3) 83 | end 84 | 85 | rep.report("factory_bot trait + override") do 86 | FactoryBot.build(:foo, :big, bar: 3) 87 | end 88 | rep.compare! 89 | end 90 | 91 | Benchmark.ips(quiet: true) do |rep| 92 | rep.report("makery delayed") do 93 | Makery[delayed].call 94 | end 95 | 96 | rep.report("factory_bot delayed") do 97 | FactoryBot.build(:delayed) 98 | end 99 | rep.compare! 100 | end 101 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rspec 2 | 3 | Style/StringLiterals: 4 | EnforcedStyle: double_quotes 5 | 6 | Metrics/BlockLength: 7 | ExcludedMethods: 8 | - it 9 | - describe 10 | - before 11 | 12 | RSpec/MultipleExpectations: 13 | Max: 4 14 | 15 | RSpec/ExampleLength: 16 | Max: 10 17 | 18 | Gemspec/RequiredRubyVersion: 19 | Enabled: false 20 | 21 | Layout/EmptyLinesAroundAttributeAccessor: 22 | Enabled: false 23 | Layout/SpaceAroundMethodCallOperator: 24 | Enabled: true 25 | Lint/BinaryOperatorWithIdenticalOperands: 26 | Enabled: true 27 | Lint/DeprecatedOpenSSLConstant: 28 | Enabled: true 29 | Lint/DuplicateElsifCondition: 30 | Enabled: true 31 | Lint/DuplicateRescueException: 32 | Enabled: true 33 | Lint/EmptyConditionalBody: 34 | Enabled: true 35 | Lint/FloatComparison: 36 | Enabled: true 37 | Lint/MissingSuper: 38 | Enabled: true 39 | Lint/MixedRegexpCaptureTypes: 40 | Enabled: true 41 | Lint/OutOfRangeRegexpRef: 42 | Enabled: true 43 | Lint/RaiseException: 44 | Enabled: true 45 | Lint/SelfAssignment: 46 | Enabled: true 47 | Lint/StructNewOverride: 48 | Enabled: true 49 | Lint/TopLevelReturnWithArgument: 50 | Enabled: true 51 | Lint/UnreachableLoop: 52 | Enabled: true 53 | Style/AccessorGrouping: 54 | Enabled: true 55 | Style/ArrayCoercion: 56 | Enabled: true 57 | Style/BisectedAttrAccessor: 58 | Enabled: true 59 | Style/CaseLikeIf: 60 | Enabled: true 61 | Style/ExplicitBlockArgument: 62 | Enabled: true 63 | Style/ExponentialNotation: 64 | Enabled: true 65 | Style/GlobalStdStream: 66 | Enabled: true 67 | Style/HashAsLastArrayItem: 68 | Enabled: true 69 | Style/HashEachMethods: 70 | Enabled: true 71 | Style/HashLikeCase: 72 | Enabled: true 73 | Style/HashTransformKeys: 74 | Enabled: true 75 | Style/HashTransformValues: 76 | Enabled: true 77 | Style/OptionalBooleanParameter: 78 | Enabled: true 79 | Style/RedundantAssignment: 80 | Enabled: true 81 | Style/RedundantFetchBlock: 82 | Enabled: true 83 | Style/RedundantFileExtensionInRequire: 84 | Enabled: true 85 | Style/RedundantRegexpCharacterClass: 86 | Enabled: true 87 | Style/RedundantRegexpEscape: 88 | Enabled: true 89 | Style/SingleArgumentDig: 90 | Enabled: true 91 | Style/SlicingWithRange: 92 | Enabled: true 93 | Style/StringConcatenation: 94 | Enabled: true 95 | # This configuration was generated by 96 | # `rubocop --auto-gen-config` 97 | # on 2021-04-07 14:35:42 UTC using RuboCop version 0.89.1. 98 | # The point is for the user to remove these configuration records 99 | # one by one as the offenses are removed from the code base. 100 | # Note that changes in the inspected code, or installation of new 101 | # versions of RuboCop, may require this file to be generated again. 102 | 103 | # Offense count: 1 104 | Style/Documentation: 105 | Exclude: 106 | - 'spec/**/*' 107 | - 'test/**/*' 108 | - 'lib/makery/builder.rb' 109 | -------------------------------------------------------------------------------- /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 kstannard@mdsol.com. 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 [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /spec/makery_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "makery" 4 | 5 | RSpec.describe Makery do 6 | let(:makery) { described_class.dup } 7 | let(:klass) { Struct.new(:name, :role, :association) } 8 | let(:maker) { makery[klass] } 9 | 10 | before do 11 | maker.base( 12 | name: "bob", 13 | role: "guest" 14 | ) 15 | 16 | maker.trait( 17 | :admin, 18 | role: "admin" 19 | ) 20 | 21 | maker.trait( 22 | :joe, 23 | name: "joe" 24 | ) 25 | 26 | maker.trait( 27 | :delayed_name, 28 | name: proc { "del" } 29 | ) 30 | 31 | maker.trait( 32 | :name_uses_role, 33 | name: ->(m) { "bob #{m[:role]}" }, 34 | role: "ceo" 35 | ) 36 | 37 | maker.trait( 38 | :role_uses_name, 39 | name: "bob", 40 | role: ->(m) { "#{m[:name]} ceo" } 41 | ) 42 | 43 | maker.trait( 44 | :with_association, 45 | association: lambda do |m| 46 | makery[klass].call(name: "#{m[:name]} bob", association: m.object) 47 | end 48 | ) 49 | end 50 | 51 | it "has a version number" do 52 | expect(described_class::VERSION).not_to be nil 53 | end 54 | 55 | it "uses the base attributes if nothing else is specified" do 56 | expect(makery[klass].call.name).to eq("bob") 57 | expect(makery[klass].call.role).to eq("guest") 58 | end 59 | 60 | it "overrides the base with any requested trait attributes" do 61 | expect(makery[klass].call(:admin).name).to eq("bob") 62 | expect(makery[klass].call(:admin).role).to eq("admin") 63 | end 64 | 65 | it "uses K:V arguments as a final override" do 66 | expect(makery[klass].call(name: "joe").name).to eq("joe") 67 | end 68 | 69 | it "objects that respond to call will be executed and the return used" do 70 | expect(makery[klass].call(:delayed_name).name).to eq("del") 71 | end 72 | 73 | it "overrides traits with K:V args" do 74 | expect(makery[klass].call(:admin, name: "joe").name).to eq("joe") 75 | expect(makery[klass].call(:admin, name: "joe").role).to eq("admin") 76 | end 77 | 78 | it "overrides attributes with traits in called order" do 79 | expect(makery[klass].call(:admin, :joe).name).to eq("joe") 80 | expect(makery[klass].call(:admin, :joe).role).to eq("admin") 81 | expect(makery[klass].call(:delayed_name, :joe).name).to eq("joe") 82 | end 83 | 84 | context "when using one attr in another with delayed execution" do 85 | it "works in any order" do 86 | expect(makery[klass].call(:name_uses_role).name) 87 | .to eq("bob ceo") 88 | expect(makery[klass].call(:role_uses_name).role) 89 | .to eq("bob ceo") 90 | end 91 | 92 | it "can be used twice with a new attr" do 93 | expect(makery[klass].call(:name_uses_role, role: "ba").name) 94 | .to eq("bob ba") 95 | expect(makery[klass].call(:name_uses_role, role: "janitor").name) 96 | .to eq("bob janitor") 97 | end 98 | end 99 | 100 | it "sends the builder as the first argument to call" do 101 | expect( 102 | makery[klass].call(name: ->(m) { "#{m[:role]} joe" }).name 103 | ).to eq("guest joe") 104 | end 105 | 106 | it "the builder's object can be used for associations" do 107 | expect( 108 | makery[klass].call(:with_association).association.name 109 | ).to eq("bob bob") 110 | 111 | expect( 112 | makery[klass].call(:delayed_name, :with_association).association.name 113 | ).to eq("del bob") 114 | 115 | o = makery[klass].call(:with_association) 116 | expect(o.association.association).to eq(o) 117 | end 118 | 119 | it "allows use of association attributes within parent" do 120 | makery[klass].trait( 121 | :use_association, 122 | name: ->(m) { "#{m[:association].name} rob" }, 123 | association: ->(m) { makery[klass].call(association: m.object) } 124 | ) 125 | 126 | expect(makery[klass].call(:use_association).name).to eq("bob rob") 127 | end 128 | 129 | it "has sequences" do 130 | expect( 131 | makery[klass].call(name: ->(m) { "user#{m.i}" }).name 132 | ).to eq("user1") 133 | expect( 134 | makery[klass].call(name: ->(m) { "user#{m.i}" }).name 135 | ).to eq("user2") 136 | end 137 | 138 | it "allows custom fatories" do 139 | makery[:custom] = ->(attrs) { attrs } 140 | expect(makery[:custom].call(foo: 1)).to eq(foo: 1) 141 | end 142 | 143 | context "when the class doesn't respond to the instantiation method" do 144 | it "throws a useful error" do 145 | expect { makery[Module.new].call }.to raise_error(NoMethodError) 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Makery [![Build Status](https://travis-ci.org/kwstannard/makery.svg?branch=master)](https://travis-ci.org/kwstannard/makery) 2 | 3 | Welcome to Makery. A lightweight and fast factory library. 4 | 5 | ## Why Makery over FactoryBot? 6 | 7 | ### Instantialize your object relationship graph without hitting the database 8 | 9 | You can use Makery's delayed execution blocks to create arbitrarily complex relationships without costly database 10 | transactions. This allows you to run tests and order of magnitude faster than equivalent tests using FactoryBot. 11 | 12 | ### Small 13 | 14 | Makery is 62 lines of code, a 96% reduction over FactoryBot. 15 | 16 | ### Speed 17 | 18 | When just initializing objects, Makery is a 12x-37x speed improvement over FactoryBot. Makery also allows you to 19 | easily set up relationships between objects without using the database, which is another order of magnitude 20 | speed boost if you are testing business logic. Run `bundle exec ruby benchmark.rb` and look at `benchmark.rb` 21 | for more details. 22 | 23 | ### ORM independence 24 | 25 | Makery is completely ORM independent. You can use it easily with any data object class and no special flags needed. 26 | 27 | ## Why FactoryBot over Makery 28 | 29 | - You like multiple factory definitions instead of traits 30 | - You need the before or after callbacks for things besides setting up associations 31 | 32 | ## Installation 33 | 34 | ```shell 35 | echo "gem 'makery'" >> Gemfile 36 | bundle 37 | ``` 38 | 39 | ## Usage 40 | 41 | ### Defining a factory 42 | 43 | Makery leverages named arguments everywhere to avoid use of DSLs. Create or fetch a factory using `Makery[YourClass]`. 44 | Then set the base attributes with `#base(attr_hash)` 45 | 46 | ```ruby 47 | class Post 48 | attr_accessor :foo, :bar 49 | end 50 | 51 | maker = Makery[Post] 52 | maker.base( 53 | foo: 1, 54 | bar: 2 55 | ) 56 | ``` 57 | 58 | ```ruby 59 | klass = Struct.new(:foo, :bar) 60 | 61 | maker = Makery[klass] 62 | maker.base( 63 | foo: 1 64 | bar: 2 65 | ) 66 | ``` 67 | 68 | ```ruby 69 | class User < ActiveRecord::Base 70 | end 71 | 72 | Makery[User].base( 73 | email: "foo@bar.com", 74 | ) 75 | ``` 76 | 77 | #### Using the factory 78 | 79 | Use `#call` to create a new object of your class. 80 | 81 | ```ruby 82 | post = Makery[Post].call 83 | post.foo #=> 1 84 | 85 | object = Makery[klass].call 86 | object.foo #=> 1 87 | 88 | Makery[User].call.email == "foo@bar.com" #=> true 89 | ``` 90 | 91 | Makery uses anything that responds to `call` for delayed execution, usually a Proc. There is a 92 | single argument passed for accessing the other attributes. You can also pass 93 | overrides into the call to maker. 94 | 95 | ```ruby 96 | Makery[klass].call(foo: ->(m) { m[:bar] + 1 }).foo == 3 #=> true 97 | ``` 98 | 99 | Makery uses traits to allow further specification of a class. Traits are merged over 100 | the base attributes. 101 | 102 | ```ruby 103 | maker = Makery[klass] 104 | maker.base( 105 | foo: 1 106 | bar: 2 107 | ) 108 | 109 | maker.trait( 110 | :big_foo, 111 | foo: 10 112 | ) 113 | 114 | Makery[klass].call(:big_foo).foo == 10 #=> true 115 | ``` 116 | 117 | #### Sequences 118 | 119 | ```ruby 120 | maker = Makery[User] 121 | maker.base( 122 | email: ->(m) { "user-#{m.i}@biz.com" } 123 | ) 124 | 125 | Makery[User].call.email #=> "user-1@biz.com" 126 | Makery[User].call.email #=> "user-2@biz.com" 127 | ``` 128 | 129 | #### Associations 130 | 131 | The object passed to call in delayed execuption provides an `object` method for creating 132 | associations between objects. Use it where you would pass the instance. 133 | 134 | For example if you have a one to many association that could be described like so: 135 | 136 | ```ruby 137 | boss = User.new 138 | employee = User.new 139 | boss.employees = [employee] 140 | ``` 141 | 142 | Makery could replicate it like this: 143 | 144 | ```ruby 145 | maker = Makery[User] 146 | maker.base( 147 | boss: ->(m) { Makery[User].call(employees: [m.object]) } 148 | ) 149 | 150 | employee = maker.call 151 | boss = employee.boss 152 | ``` 153 | 154 | ### What kinds of classes can use this? 155 | 156 | Any class used needs writer methods corresponding to each attribute and that should be it. 157 | 158 | ### How does this work behind the scenes? 159 | 160 | It is all about hashes and merging. The base attribute set is always there at the bottom and 161 | each trait merges over the base. Finally the named arguments are merged over all of that. Once 162 | that is merged, any attribute values that respond to `call` are called. Finally, an instance 163 | of the class being factoried has its attributes set from the attribute hash. 164 | 165 | ### ActiveRecord and Sequel 166 | 167 | Makery operates independently of ActiveRecord or any ORM. You could do one of the 168 | following. 169 | 170 | ```ruby 171 | maker = Makery[User] 172 | maker.base( 173 | email: "email@email.com" 174 | password: "a password" 175 | ) 176 | 177 | user = Makery[User].call 178 | user.save 179 | 180 | # or a method to handle it like FactoryBot 181 | 182 | def create(klass, *args) 183 | Makery[klass].call(*args).tap(&:save) 184 | end 185 | create(User) 186 | ``` 187 | 188 | ### Custom Factories 189 | 190 | A way to make custom factories has been provided via the `#[]=` method. Anything can be stored, 191 | but you probably want to use a proc. The following example uses a proc with default arguments to 192 | create a JSON document. 193 | 194 | ```ruby 195 | Makery["user registration request body"] = ->(username: 'joe', password: '1234') { 196 | {user: {username: username, password: password} }.to_json 197 | } 198 | 199 | Makery["user registration request body"].call 200 | ``` 201 | 202 | ## Development 203 | 204 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. 205 | 206 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 207 | 208 | ## Contributing 209 | 210 | Bug reports and pull requests are welcome on GitHub at https://github.com/kwstannard/makery. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 211 | 212 | ## License 213 | 214 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 215 | 216 | ## Code of Conduct 217 | 218 | Everyone interacting in the Makery project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/kwstannard/makery/blob/master/CODE_OF_CONDUCT.md). 219 | --------------------------------------------------------------------------------