├── VERSION ├── Gemfile ├── lib ├── machinist │ ├── version.rb │ ├── active_record.rb │ ├── strategies.rb │ ├── active_record │ │ ├── blueprint.rb │ │ └── lathe.rb │ ├── strategies │ │ ├── pass_attributes_to_new.rb │ │ └── assign_attributes.rb │ ├── exceptions.rb │ ├── lathe.rb │ ├── blueprint.rb │ └── machinable.rb ├── generators │ └── machinist │ │ ├── install │ │ ├── USAGE │ │ ├── templates │ │ │ ├── blueprints.rb │ │ │ └── machinist.rb.erb │ │ └── install_generator.rb │ │ └── model │ │ └── model_generator.rb └── machinist.rb ├── .gitignore ├── spec ├── spec_helper.rb ├── strategies_spec.rb ├── exceptions_spec.rb ├── support │ └── active_record_environment.rb ├── blueprint_spec.rb ├── machinable_spec.rb ├── active_record_spec.rb └── inheritance_spec.rb ├── Rakefile ├── .github └── workflows │ └── test.yml ├── machinist.gemspec ├── MIT-LICENSE └── README.markdown /VERSION: -------------------------------------------------------------------------------- 1 | 2.0 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /lib/machinist/version.rb: -------------------------------------------------------------------------------- 1 | module Machinist 2 | VERSION = "2.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/machinist/install/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Copy Machinist files to your application. 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.bundle 3 | /.rvmrc 4 | /coverage 5 | /doc 6 | /pkg 7 | /tags 8 | /Gemfile.lock 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib' 2 | $LOAD_PATH.unshift File.dirname(__FILE__) 3 | 4 | require 'rubygems' 5 | require 'rspec' 6 | require 'machinist' 7 | -------------------------------------------------------------------------------- /lib/generators/machinist/install/templates/blueprints.rb: -------------------------------------------------------------------------------- 1 | require 'machinist/active_record' 2 | 3 | # Add your blueprints here. 4 | # 5 | # e.g. 6 | # Post.blueprint do 7 | # title { "Post #{sn}" } 8 | # body { "Lorem ipsum..." } 9 | # end 10 | -------------------------------------------------------------------------------- /lib/machinist.rb: -------------------------------------------------------------------------------- 1 | require 'machinist/blueprint' 2 | require 'machinist/exceptions' 3 | require 'machinist/lathe' 4 | require 'machinist/machinable' 5 | require 'machinist/strategies' 6 | require 'machinist/strategies/assign_attributes' 7 | require 'machinist/strategies/pass_attributes_to_new' 8 | 9 | -------------------------------------------------------------------------------- /lib/generators/machinist/install/templates/machinist.rb.erb: -------------------------------------------------------------------------------- 1 | <%- if rspec? -%> 2 | # Load the blueprints from over in spec support. 3 | require "#{Rails.root}/spec/support/blueprints" 4 | <%- else -%> 5 | # Load the blueprints from over in test. 6 | require "#{Rails.root}/test/blueprints" 7 | <%- end -%> 8 | -------------------------------------------------------------------------------- /lib/machinist/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'machinist' 3 | require 'machinist/active_record/blueprint' 4 | require 'machinist/active_record/lathe' 5 | 6 | module ActiveRecord #:nodoc: 7 | class Base #:nodoc: 8 | extend Machinist::Machinable 9 | 10 | def self.blueprint_class 11 | Machinist::ActiveRecord::Blueprint 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/machinist/strategies.rb: -------------------------------------------------------------------------------- 1 | module Machinist::Strategies 2 | 3 | def self.register(name, strategy) 4 | @strategies ||= {} 5 | @strategies[name] = strategy 6 | end 7 | 8 | def self.[](name) 9 | @strategies ||= {} 10 | @strategies[name] 11 | end 12 | 13 | # FIXME: Make this configurable. 14 | def self.default 15 | :assign_attributes 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/machinist/active_record/blueprint.rb: -------------------------------------------------------------------------------- 1 | module Machinist::ActiveRecord 2 | class Blueprint < Machinist::Blueprint 3 | 4 | # Make and save an object. 5 | def make!(attributes = {}) 6 | object = make(attributes) 7 | object.save! 8 | object.reload 9 | end 10 | 11 | def lathe_class #:nodoc: 12 | Machinist::ActiveRecord::Lathe 13 | end 14 | 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/machinist/model/model_generator.rb: -------------------------------------------------------------------------------- 1 | module Machinist 2 | module Generators #:nodoc: 3 | class ModelGenerator < Rails::Generators::NamedBase #:nodoc: 4 | argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" 5 | 6 | def create_blueprint 7 | append_file "spec/support/blueprints.rb", "\n#{class_name}.blueprint do\n # Attributes here\nend\n" 8 | end 9 | 10 | end 11 | end 12 | end 13 | 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | Bundler::GemHelper.install_tasks 4 | 5 | require 'rake' 6 | require 'rspec/core/rake_task' 7 | require 'rdoc/task' 8 | 9 | 10 | RSpec::Core::RakeTask.new(:spec) 11 | 12 | desc 'Run the specs.' 13 | task :default => :spec 14 | 15 | 16 | RDoc::Task.new(:rdoc) do |rdoc| 17 | rdoc.rdoc_dir = 'doc' 18 | rdoc.title = 'Machinist' 19 | rdoc.options << '--line-numbers' 20 | rdoc.rdoc_files.include('lib') 21 | end 22 | 23 | task :notes do 24 | system "grep -n -r 'FIXME\\|TODO' lib spec" 25 | end 26 | -------------------------------------------------------------------------------- /spec/strategies_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | RSpec.describe Machinist::Strategies::PassAttributesToNew do 4 | 5 | it "should construct an object by passing attributes to new" do 6 | klass = Class.new do 7 | extend Machinist::Machinable 8 | 9 | def initialize(attributes = {}) 10 | @title = attributes[:title] 11 | end 12 | 13 | attr_reader :title 14 | end 15 | 16 | klass.blueprint(:strategy => :pass_attributes_to_new) do 17 | title { "A Title" } 18 | end 19 | 20 | expect(klass.make.title).to eq("A Title") 21 | end 22 | 23 | 24 | end 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/machinist/strategies/pass_attributes_to_new.rb: -------------------------------------------------------------------------------- 1 | module Machinist::Strategies 2 | 3 | # If you have a blueprint: 4 | # 5 | # Post.blueprint(:strategy => :pass_attributes_to_new) do 6 | # title { "A Post" } 7 | # body { "Lorem ipsum..." } 8 | # end 9 | # 10 | # this strategy is the equivalent of: 11 | # 12 | # Post.new(:title => "A Post", :body => "Lorem ipsum...") 13 | # 14 | module PassAttributesToNew 15 | 16 | def finalised_object #:nodoc: 17 | @klass.new(@assigned_attributes) 18 | end 19 | 20 | end 21 | 22 | register :pass_attributes_to_new, PassAttributesToNew 23 | 24 | end 25 | 26 | -------------------------------------------------------------------------------- /lib/machinist/active_record/lathe.rb: -------------------------------------------------------------------------------- 1 | module Machinist::ActiveRecord 2 | 3 | class Lathe < Machinist::Lathe 4 | 5 | def make_one_value(attribute, args) #:nodoc: 6 | if block_given? 7 | raise_argument_error(attribute) unless args.empty? 8 | yield 9 | else 10 | make_association(attribute, args) 11 | end 12 | end 13 | 14 | def make_association(attribute, args) #:nodoc: 15 | association = @klass.reflect_on_association(attribute) 16 | if association 17 | association.klass.make(*args) 18 | else 19 | raise_argument_error(attribute) 20 | end 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/exceptions_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | RSpec.describe Machinist, "exceptions" do 4 | 5 | describe Machinist::BlueprintCantSaveError do 6 | it "presents the right message" do 7 | blueprint = Machinist::Blueprint.new(String) { } 8 | exception = Machinist::BlueprintCantSaveError.new(blueprint) 9 | expect(exception.message).to eq("make! is not supported by blueprints for class String") 10 | end 11 | end 12 | 13 | describe Machinist::NoBlueprintError do 14 | it "presents the right message" do 15 | exception = Machinist::NoBlueprintError.new(String, :master) 16 | expect(exception.message).to eq("No master blueprint defined for class String") 17 | end 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/machinist/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Machinist 2 | 3 | # Raised when make! is called on a class whose blueprints don't support 4 | # saving. 5 | class BlueprintCantSaveError < RuntimeError 6 | attr_reader :blueprint 7 | 8 | def initialize(blueprint) 9 | @blueprint = blueprint 10 | end 11 | 12 | def message 13 | "make! is not supported by blueprints for class #{@blueprint.klass.name}" 14 | end 15 | end 16 | 17 | # Raised when calling make on a class with no corresponding blueprint 18 | # defined. 19 | class NoBlueprintError < RuntimeError 20 | attr_reader :klass, :name 21 | 22 | def initialize(klass, name) 23 | @klass = klass 24 | @name = name 25 | end 26 | 27 | def message 28 | "No #{@name} blueprint defined for class #{@klass.name}" 29 | end 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tests 3 | on: [ push, pull_request ] 4 | jobs: 5 | test: 6 | name: Test (Ruby ${{ matrix.ruby }}) 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: [ '2.6', '2.7', '3.0', '3.1', '3.2', '3.3' ] 12 | services: 13 | mysql: 14 | image: mysql 15 | env: 16 | MYSQL_DATABASE: machinist 17 | MYSQL_ALLOW_EMPTY_PASSWORD: true 18 | ports: 19 | - 3306:3306 20 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 21 | env: 22 | MYSQL_HOST: 127.0.0.1 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby }} 28 | bundler-cache: true 29 | - run: bundle exec rspec 30 | -------------------------------------------------------------------------------- /lib/machinist/strategies/assign_attributes.rb: -------------------------------------------------------------------------------- 1 | module Machinist::Strategies 2 | 3 | # If you have a blueprint: 4 | # 5 | # Post.blueprint(:strategy => :assign_attributes) do 6 | # title { "A Post" } 7 | # body { "Lorem ipsum..." } 8 | # end 9 | # 10 | # this strategy is the equivalent of: 11 | # 12 | # post = Post.new 13 | # post.title = "A Post" 14 | # post.body = "Lorem ipsum..." 15 | # 16 | module AssignAttributes 17 | 18 | def prepare #:nodoc: 19 | @object = @klass.new 20 | end 21 | 22 | # Call this within the blueprint to access to the object under construction. 23 | attr_reader :object 24 | 25 | def assign_attribute(key, value) #:nodoc: 26 | super 27 | @object.send("#{key}=", value) 28 | end 29 | 30 | alias_method :finalised_object, :object 31 | 32 | end 33 | 34 | register :assign_attributes, AssignAttributes 35 | 36 | end 37 | -------------------------------------------------------------------------------- /machinist.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "machinist/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "machinist" 7 | s.version = Machinist::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Pete Yandell"] 10 | s.email = ["pete@notahat.com"] 11 | s.homepage = "http://github.com/notahat/machinist" 12 | s.summary = "Fixtures aren't fun. Machinist is." 13 | 14 | s.files = `git ls-files`.split("\n") 15 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 16 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 17 | s.require_paths = ["lib"] 18 | 19 | s.add_development_dependency "activerecord" 20 | s.add_development_dependency "mysql2" 21 | s.add_development_dependency "rake" 22 | s.add_development_dependency "rspec", "~> 3" 23 | s.add_development_dependency "rdoc" 24 | end 25 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2010 Peter Yandell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /lib/generators/machinist/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | module Machinist 2 | module Generators #:nodoc: 3 | class InstallGenerator < Rails::Generators::Base #:nodoc: 4 | source_root File.expand_path('../templates', __FILE__) 5 | 6 | class_option :test_framework, :type => :string, :aliases => "-t", :desc => "Test framework to use Machinist with" 7 | class_option :cucumber, :type => :boolean, :desc => "Set up access to Machinist from Cucumber" 8 | 9 | def blueprints_file 10 | if rspec? 11 | copy_file "blueprints.rb", "spec/support/blueprints.rb" 12 | else 13 | copy_file "blueprints.rb", "test/blueprints.rb" 14 | end 15 | end 16 | 17 | def test_helper 18 | if test_unit? 19 | inject_into_file("test/test_helper.rb", :after => "require 'rails/test_help'\n") do 20 | "require File.expand_path(File.dirname(__FILE__) + '/blueprints')\n" 21 | end 22 | end 23 | end 24 | 25 | def cucumber_support 26 | if cucumber? 27 | template "machinist.rb.erb", "features/support/machinist.rb" 28 | end 29 | end 30 | 31 | private 32 | 33 | def rspec? 34 | options[:test_framework].to_sym == :rspec 35 | end 36 | 37 | def test_unit? 38 | options[:test_framework].to_sym == :test_unit 39 | end 40 | 41 | def cucumber? 42 | options[:cucumber] 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/support/active_record_environment.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'machinist/active_record' 3 | 4 | ActiveRecord::Base.establish_connection( 5 | :adapter => "mysql2", 6 | :host => ENV.fetch("MYSQL_HOST", "localhost"), 7 | :database => "machinist", 8 | :username => "root", 9 | :password => "" 10 | ) 11 | 12 | ActiveRecord::Schema.define(:version => 0) do 13 | create_table :users, :force => true do |t| 14 | t.column :username, :string 15 | end 16 | 17 | create_table :posts, :force => true do |t| 18 | t.column :title, :string 19 | t.column :author_id, :integer 20 | t.column :body, :text 21 | end 22 | 23 | create_table :comments, :force => true do |t| 24 | t.column :post_id, :integer 25 | t.column :body, :text 26 | end 27 | 28 | create_table :tags, :force => true do |t| 29 | t.column :name, :string 30 | end 31 | 32 | create_table :posts_tags, :id => false, :force => true do |t| 33 | t.column :post_id, :integer 34 | t.column :tag_id, :integer 35 | end 36 | end 37 | 38 | class User < ActiveRecord::Base 39 | validates_presence_of :username 40 | validates_uniqueness_of :username 41 | end 42 | 43 | class Post < ActiveRecord::Base 44 | has_many :comments 45 | belongs_to :author, :class_name => "User" 46 | has_and_belongs_to_many :tags 47 | end 48 | 49 | class Comment < ActiveRecord::Base 50 | belongs_to :post 51 | end 52 | 53 | class Tag < ActiveRecord::Base 54 | has_and_belongs_to_many :posts 55 | end 56 | 57 | module ActiveRecordEnvironment 58 | 59 | def empty_database! 60 | [User, Post, Comment].each do |klass| 61 | klass.delete_all 62 | klass.clear_blueprints! 63 | end 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /lib/machinist/lathe.rb: -------------------------------------------------------------------------------- 1 | module Machinist 2 | 3 | # When you make an object, the blueprint for that object is instance-evaled 4 | # against a Lathe. 5 | # 6 | # The Lathe implements all the methods that are available to the blueprint, 7 | # including method_missing to let the blueprint define attributes. 8 | class Lathe 9 | 10 | def initialize(klass, strategy, serial_number, attributes = {}) 11 | @klass = klass 12 | @strategy = strategy 13 | @serial_number = serial_number 14 | @assigned_attributes = {} 15 | 16 | self.extend(Strategies[strategy]) 17 | prepare 18 | attributes.each {|key, value| assign_attribute(key, value) } 19 | end 20 | 21 | # Returns a unique serial number for the object under construction. 22 | def sn 23 | @serial_number 24 | end 25 | 26 | # Returns the object under construction. 27 | attr_reader :object 28 | 29 | def method_missing(attribute, *args, &block) #:nodoc: 30 | unless attribute_assigned?(attribute) 31 | assign_attribute(attribute, make_attribute(attribute, args, &block)) 32 | end 33 | end 34 | 35 | # Undef a couple of methods that are common ActiveRecord attributes. 36 | # (Both of these are deprecated in Ruby 1.8 anyway.) 37 | undef_method :id if respond_to?(:id) 38 | undef_method :type if respond_to?(:type) 39 | 40 | protected 41 | 42 | # Called before any attributes are assigned. Strategies can override this 43 | # to do setup. 44 | def prepare 45 | end 46 | 47 | def make_attribute(attribute, args, &block) #:nodoc: 48 | count = args.shift if args.first.is_a?(Integer) 49 | if count 50 | Array.new(count) { make_one_value(attribute, args, &block) } 51 | else 52 | make_one_value(attribute, args, &block) 53 | end 54 | end 55 | 56 | def make_one_value(attribute, args) #:nodoc: 57 | raise_argument_error(attribute) unless args.empty? 58 | yield 59 | end 60 | 61 | def assign_attribute(key, value) #:nodoc: 62 | @assigned_attributes[key.to_sym] = value 63 | end 64 | 65 | def attribute_assigned?(key) #:nodoc: 66 | @assigned_attributes.has_key?(key.to_sym) 67 | end 68 | 69 | def raise_argument_error(attribute) #:nodoc: 70 | raise ArgumentError.new("Invalid arguments to attribute #{attribute} in blueprint") 71 | end 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/blueprint_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | require 'ostruct' 3 | 4 | RSpec.describe Machinist::Blueprint do 5 | 6 | it "makes an object of the given class" do 7 | blueprint = Machinist::Blueprint.new(OpenStruct) { } 8 | expect(blueprint.make).to be_an(OpenStruct) 9 | end 10 | 11 | it "constructs an attribute from the blueprint" do 12 | blueprint = Machinist::Blueprint.new(OpenStruct) do 13 | name { "Fred" } 14 | end 15 | expect(blueprint.make.name).to eq("Fred") 16 | end 17 | 18 | it "constructs an array for an attribute in the blueprint" do 19 | blueprint = Machinist::Blueprint.new(OpenStruct) do 20 | things(3) { Object.new } 21 | end 22 | things = blueprint.make.things 23 | expect(things).to be_an(Array) 24 | expect(things.size).to eq(3) 25 | things.each {|thing| expect(thing).to be_an(Object) } 26 | expect(things.uniq).to eq(things) 27 | end 28 | 29 | it "allows passing in attributes to override the blueprint" do 30 | block_called = false 31 | blueprint = Machinist::Blueprint.new(OpenStruct) do 32 | name { block_called = true; "Fred" } 33 | end 34 | expect(blueprint.make(:name => "Bill").name).to eq("Bill") 35 | expect(block_called).to be_falsey 36 | end 37 | 38 | it "provides a serial number within the blueprint" do 39 | blueprint = Machinist::Blueprint.new(OpenStruct) do 40 | name { "Fred #{sn}" } 41 | end 42 | expect(blueprint.make.name).to eq("Fred 0001") 43 | expect(blueprint.make.name).to eq("Fred 0002") 44 | end 45 | 46 | it "provides access to the object being constructed within the blueprint" do 47 | blueprint = Machinist::Blueprint.new(OpenStruct) do 48 | title { "Test" } 49 | body { object.title } 50 | end 51 | expect(blueprint.make.body).to eq("Test") 52 | end 53 | 54 | it "allows attribute names to be strings" do 55 | blueprint = Machinist::Blueprint.new(OpenStruct) do 56 | name { "Fred" } 57 | end 58 | expect(blueprint.make("name" => "Bill").name).to eq("Bill") 59 | end 60 | 61 | # These are normally a problem because of name clashes with the standard (but 62 | # deprecated) Ruby methods. This test makes sure we work around this. 63 | it "works with type and id attributes" do 64 | klass = Class.new do 65 | attr_accessor :id, :type 66 | end 67 | blueprint = Machinist::Blueprint.new(klass) do 68 | id { "custom id" } 69 | type { "custom type" } 70 | end 71 | object = blueprint.make 72 | expect(object.id).to eq("custom id") 73 | expect(object.type).to eq("custom type") 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /lib/machinist/blueprint.rb: -------------------------------------------------------------------------------- 1 | module Machinist 2 | 3 | # A Blueprint defines a method of constructing objects of a particular class. 4 | class Blueprint 5 | 6 | # Construct a blueprint for the given +klass+. 7 | # 8 | # Pass in the +:parent+ option to define a parent blueprint to apply after 9 | # this one. You can supply another blueprint, or a class in which to look 10 | # for a blueprint. In the latter case, make will walk up the superclass 11 | # chain looking for blueprints to apply. 12 | def initialize(klass, options = {}, &block) 13 | @klass = klass 14 | @parent = options[:parent] 15 | @strategy = options[:strategy] || Strategies.default 16 | @block = block 17 | end 18 | 19 | attr_reader :klass, :parent, :block 20 | 21 | # Generate an object from this blueprint. 22 | # 23 | # Pass in attributes to override values defined in the blueprint. 24 | def make(attributes = {}) 25 | lathe = lathe_class.new(@klass, @strategy, new_serial_number, attributes) 26 | 27 | lathe.instance_eval(&@block) 28 | each_ancestor {|blueprint| lathe.instance_eval(&blueprint.block) } 29 | 30 | lathe.finalised_object 31 | end 32 | 33 | # Returns the Lathe class used to make objects for this blueprint. 34 | # 35 | # Subclasses can override this to substitute a custom lathe class. 36 | def lathe_class 37 | Lathe 38 | end 39 | 40 | # Returns the parent blueprint for this blueprint. 41 | def parent_blueprint 42 | case @parent 43 | when nil 44 | nil 45 | when Blueprint 46 | # @parent references the parent blueprint directly. 47 | @parent 48 | else 49 | # @parent is a class in which we should look for a blueprint. 50 | find_blueprint_in_superclass_chain(@parent) 51 | end 52 | end 53 | 54 | # Yields the parent blueprint, its parent blueprint, etc. 55 | def each_ancestor 56 | ancestor = parent_blueprint 57 | while ancestor 58 | yield ancestor 59 | ancestor = ancestor.parent_blueprint 60 | end 61 | end 62 | 63 | protected 64 | 65 | def new_serial_number #:nodoc: 66 | parent_blueprint = self.parent_blueprint # Cache this for speed. 67 | if parent_blueprint 68 | parent_blueprint.new_serial_number 69 | else 70 | @serial_number ||= 0 71 | @serial_number += 1 72 | sprintf("%04d", @serial_number) 73 | end 74 | end 75 | 76 | private 77 | 78 | def find_blueprint_in_superclass_chain(klass) 79 | until has_blueprint?(klass) || klass.nil? 80 | klass = klass.superclass 81 | end 82 | klass && klass.blueprint 83 | end 84 | 85 | def has_blueprint?(klass) 86 | klass.respond_to?(:blueprint) && !klass.blueprint.nil? 87 | end 88 | 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/machinable_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | module MachinableSpecs 4 | class Post 5 | extend Machinist::Machinable 6 | attr_accessor :title, :body, :comments 7 | end 8 | 9 | class Comment 10 | extend Machinist::Machinable 11 | attr_accessor :post, :title 12 | end 13 | end 14 | 15 | RSpec.describe Machinist::Machinable do 16 | 17 | before(:each) do 18 | MachinableSpecs::Post.clear_blueprints! 19 | end 20 | 21 | it "makes an object" do 22 | MachinableSpecs::Post.blueprint do 23 | title { "First Post" } 24 | end 25 | 26 | post = MachinableSpecs::Post.make 27 | expect(post).to be_a(MachinableSpecs::Post) 28 | expect(post.title).to eq("First Post") 29 | end 30 | 31 | it "makes an object from a named blueprint" do 32 | MachinableSpecs::Post.blueprint do 33 | title { "First Post" } 34 | body { "Woot!" } 35 | end 36 | 37 | MachinableSpecs::Post.blueprint(:extra) do 38 | title { "Extra!" } 39 | end 40 | 41 | post = MachinableSpecs::Post.make(:extra) 42 | expect(post).to be_a(MachinableSpecs::Post) 43 | expect(post.title).to eq("Extra!") 44 | expect(post.body).to eq("Woot!") 45 | end 46 | 47 | it "makes an array of objects" do 48 | MachinableSpecs::Post.blueprint do 49 | title { "First Post" } 50 | end 51 | 52 | posts = MachinableSpecs::Post.make(3) 53 | expect(posts).to be_an(Array) 54 | expect(posts.size).to eq(3) 55 | posts.each do |post| 56 | expect(post).to be_a(MachinableSpecs::Post) 57 | expect(post.title).to eq("First Post") 58 | end 59 | end 60 | 61 | it "makes array attributes from the blueprint" do 62 | MachinableSpecs::Comment.blueprint { } 63 | MachinableSpecs::Post.blueprint do 64 | comments(3) { MachinableSpecs::Comment.make } 65 | end 66 | 67 | post = MachinableSpecs::Post.make 68 | expect(post.comments).to be_a(Array) 69 | expect(post.comments.size).to eq(3) 70 | post.comments.each do |comment| 71 | expect(comment).to be_a(MachinableSpecs::Comment) 72 | end 73 | end 74 | 75 | it "fails without a blueprint" do 76 | expect do 77 | MachinableSpecs::Post.make 78 | end.to raise_error(Machinist::NoBlueprintError) do |exception| 79 | expect(exception.klass).to eq(MachinableSpecs::Post) 80 | expect(exception.name).to eq(:master) 81 | end 82 | 83 | expect do 84 | MachinableSpecs::Post.make(:some_name) 85 | end.to raise_error(Machinist::NoBlueprintError) do |exception| 86 | expect(exception.klass).to eq(MachinableSpecs::Post) 87 | expect(exception.name).to eq(:some_name) 88 | end 89 | end 90 | 91 | it "fails when calling make! on an unsavable object" do 92 | MachinableSpecs::Post.blueprint { } 93 | 94 | expect do 95 | MachinableSpecs::Post.make! 96 | end.to raise_error(Machinist::BlueprintCantSaveError) do |exception| 97 | expect(exception.blueprint.klass).to eq(MachinableSpecs::Post) 98 | end 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /spec/active_record_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | require 'support/active_record_environment' 3 | 4 | RSpec.describe Machinist::ActiveRecord do 5 | include ActiveRecordEnvironment 6 | 7 | before(:each) do 8 | empty_database! 9 | end 10 | 11 | context "make" do 12 | it "returns an unsaved object" do 13 | Post.blueprint { } 14 | post = Post.make 15 | expect(post).to be_a(Post) 16 | expect(post).to be_new_record 17 | end 18 | end 19 | 20 | context "make!" do 21 | it "makes and saves objects" do 22 | Post.blueprint { } 23 | post = Post.make! 24 | expect(post).to be_a(Post) 25 | expect(post).not_to be_new_record 26 | end 27 | 28 | it "raises an exception for an invalid object" do 29 | User.blueprint { } 30 | expect { 31 | User.make!(:username => "") 32 | }.to raise_error(ActiveRecord::RecordInvalid) 33 | end 34 | end 35 | 36 | context "associations support" do 37 | it "handles belongs_to associations" do 38 | User.blueprint do 39 | username { "user_#{sn}" } 40 | end 41 | Post.blueprint do 42 | author 43 | end 44 | post = Post.make! 45 | expect(post).to be_a(Post) 46 | expect(post).not_to be_new_record 47 | expect(post.author).to be_a(User) 48 | expect(post.author).not_to be_new_record 49 | end 50 | 51 | it "handles has_many associations" do 52 | Post.blueprint do 53 | comments(3) 54 | end 55 | Comment.blueprint { } 56 | post = Post.make! 57 | expect(post).to be_a(Post) 58 | expect(post).not_to be_new_record 59 | expect(post.comments.size).to eq(3) 60 | post.comments.each do |comment| 61 | expect(comment).to be_a(Comment) 62 | expect(comment).not_to be_new_record 63 | end 64 | end 65 | 66 | it "handles habtm associations" do 67 | Post.blueprint do 68 | tags(3) 69 | end 70 | Tag.blueprint do 71 | name { "tag_#{sn}" } 72 | end 73 | post = Post.make! 74 | expect(post).to be_a(Post) 75 | expect(post).not_to be_new_record 76 | expect(post.tags.size).to eq(3) 77 | post.tags.each do |tag| 78 | expect(tag).to be_a(Tag) 79 | expect(tag).not_to be_new_record 80 | end 81 | end 82 | 83 | it "handles overriding associations" do 84 | User.blueprint do 85 | username { "user_#{sn}" } 86 | end 87 | Post.blueprint do 88 | author { User.make(:username => "post_author_#{sn}") } 89 | end 90 | post = Post.make! 91 | expect(post).to be_a(Post) 92 | expect(post).not_to be_new_record 93 | expect(post.author).to be_a(User) 94 | expect(post.author).not_to be_new_record 95 | expect(post.author.username).to match(/^post_author_\d+$/) 96 | end 97 | end 98 | 99 | context "error handling" do 100 | it "raises an exception for an attribute with no value" do 101 | User.blueprint { username } 102 | expect { 103 | User.make 104 | }.to raise_error(ArgumentError) 105 | end 106 | end 107 | 108 | end 109 | -------------------------------------------------------------------------------- /spec/inheritance_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | require 'ostruct' 3 | 4 | module InheritanceSpecs 5 | class Grandpa 6 | extend Machinist::Machinable 7 | attr_accessor :name, :age 8 | end 9 | 10 | class Dad < Grandpa 11 | extend Machinist::Machinable 12 | attr_accessor :name, :age 13 | end 14 | 15 | class Son < Dad 16 | extend Machinist::Machinable 17 | attr_accessor :name, :age 18 | end 19 | end 20 | 21 | RSpec.describe Machinist::Blueprint do 22 | 23 | describe "explicit inheritance" do 24 | it "inherits attributes from the parent blueprint" do 25 | parent_blueprint = Machinist::Blueprint.new(OpenStruct) do 26 | name { "Fred" } 27 | age { 97 } 28 | end 29 | 30 | child_blueprint = Machinist::Blueprint.new(OpenStruct, :parent => parent_blueprint) do 31 | name { "Bill" } 32 | end 33 | 34 | child = child_blueprint.make 35 | expect(child.name).to eq("Bill") 36 | expect(child.age).to eq(97) 37 | end 38 | 39 | it "takes the serial number from the parent" do 40 | parent_blueprint = Machinist::Blueprint.new(OpenStruct) do 41 | parent_serial { sn } 42 | end 43 | 44 | child_blueprint = Machinist::Blueprint.new(OpenStruct, :parent => parent_blueprint) do 45 | child_serial { sn } 46 | end 47 | 48 | expect(parent_blueprint.make.parent_serial).to eq("0001") 49 | expect(child_blueprint.make.child_serial).to eq("0002") 50 | expect(parent_blueprint.make.parent_serial).to eq("0003") 51 | end 52 | end 53 | 54 | describe "class inheritance" do 55 | before(:each) do 56 | [InheritanceSpecs::Grandpa, InheritanceSpecs::Dad, InheritanceSpecs::Son].each(&:clear_blueprints!) 57 | end 58 | 59 | it "inherits blueprinted attributes from the parent class" do 60 | InheritanceSpecs::Dad.blueprint do 61 | name { "Fred" } 62 | end 63 | InheritanceSpecs::Son.blueprint { } 64 | expect(InheritanceSpecs::Son.make.name).to eq("Fred") 65 | end 66 | 67 | it "overrides blueprinted attributes in the child class" do 68 | InheritanceSpecs::Dad.blueprint do 69 | name { "Fred" } 70 | end 71 | InheritanceSpecs::Son.blueprint do 72 | name { "George" } 73 | end 74 | expect(InheritanceSpecs::Dad.make.name).to eq("Fred") 75 | expect(InheritanceSpecs::Son.make.name).to eq("George") 76 | end 77 | 78 | it "inherits from blueprinted attributes in ancestor class" do 79 | InheritanceSpecs::Grandpa.blueprint do 80 | name { "Fred" } 81 | end 82 | InheritanceSpecs::Son.blueprint { } 83 | expect(InheritanceSpecs::Grandpa.make.name).to eq("Fred") 84 | expect { InheritanceSpecs::Dad.make }.to raise_error(RuntimeError) 85 | expect(InheritanceSpecs::Son.make.name).to eq("Fred") 86 | end 87 | 88 | it "follows inheritance for named blueprints correctly" do 89 | InheritanceSpecs::Dad.blueprint do 90 | name { "John" } 91 | age { 56 } 92 | end 93 | InheritanceSpecs::Dad.blueprint(:special) do 94 | name { "Paul" } 95 | end 96 | InheritanceSpecs::Son.blueprint(:special) do 97 | age { 37 } 98 | end 99 | expect(InheritanceSpecs::Son.make(:special).name).to eq("John") 100 | expect(InheritanceSpecs::Son.make(:special).age).to eq(37) 101 | end 102 | 103 | it "should allow overriding the parent" 104 | 105 | end 106 | 107 | end 108 | -------------------------------------------------------------------------------- /lib/machinist/machinable.rb: -------------------------------------------------------------------------------- 1 | module Machinist 2 | 3 | # Extend classes with this module to define the blueprint and make methods. 4 | module Machinable 5 | # Define a blueprint with the given name for this class. 6 | # 7 | # e.g. 8 | # Post.blueprint do 9 | # title { "A Post" } 10 | # body { "Lorem ipsum..." } 11 | # end 12 | # 13 | # If you provide the +name+ argument, a named blueprint will be created. 14 | # See the +blueprint_name+ argument to the make method. 15 | def blueprint(*args, &block) 16 | shift_arg = lambda {|klass| args.shift if args.first.is_a?(klass) } 17 | name = shift_arg[Symbol] || :master 18 | options = shift_arg[Hash] || {} 19 | raise ArgumentError unless args.empty? # FIXME: Meaningful exception. 20 | 21 | @blueprints ||= {} 22 | if block_given? 23 | options[:parent] ||= (name == :master ? superclass : self) 24 | @blueprints[name] = blueprint_class.new(self, options, &block) 25 | end 26 | @blueprints[name] 27 | end 28 | 29 | # Construct an object from a blueprint. 30 | # 31 | # :call-seq: 32 | # make([count], [blueprint_name], [attributes = {}]) 33 | # 34 | # [+count+] 35 | # The number of objects to construct. If +count+ is provided, make 36 | # returns an array of objects rather than a single object. 37 | # [+blueprint_name+] 38 | # Construct the object from the named blueprint, rather than the master 39 | # blueprint. 40 | # [+attributes+] 41 | # Override the attributes from the blueprint with values from this hash. 42 | def make(*args) 43 | decode_args_to_make(*args) do |blueprint, attributes| 44 | blueprint.make(attributes) 45 | end 46 | end 47 | 48 | # Construct and save an object from a blueprint, if the class allows saving. 49 | # 50 | # :call-seq: 51 | # make!([count], [blueprint_name], [attributes = {}]) 52 | # 53 | # Arguments are the same as for make. 54 | def make!(*args) 55 | decode_args_to_make(*args) do |blueprint, attributes| 56 | raise BlueprintCantSaveError.new(blueprint) unless blueprint.respond_to?(:make!) 57 | blueprint.make!(attributes) 58 | end 59 | end 60 | 61 | # Remove all blueprints defined on this class. 62 | def clear_blueprints! 63 | @blueprints = {} 64 | end 65 | 66 | # Classes that include Machinable can override this method if they want to 67 | # use a custom blueprint class when constructing blueprints. 68 | # 69 | # The default is Machinist::Blueprint. 70 | def blueprint_class 71 | Machinist::Blueprint 72 | end 73 | 74 | private 75 | 76 | # Parses the arguments to make. 77 | # 78 | # Yields a blueprint and an attributes hash to the block, which should 79 | # construct an object from them. The block may be called multiple times to 80 | # construct multiple objects. 81 | def decode_args_to_make(*args) #:nodoc: 82 | shift_arg = lambda {|klass| args.shift if args.first.is_a?(klass) } 83 | count = shift_arg[Integer] 84 | name = shift_arg[Symbol] || :master 85 | attributes = shift_arg[Hash] || {} 86 | raise ArgumentError.new("Couldn't understand arguments") unless args.empty? 87 | 88 | @blueprints ||= {} 89 | blueprint = @blueprints[name] 90 | raise NoBlueprintError.new(self, name) unless blueprint 91 | 92 | if count.nil? 93 | yield(blueprint, attributes) 94 | else 95 | Array.new(count) { yield(blueprint, attributes) } 96 | end 97 | end 98 | 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Machinist 2 2 | 3 | *Fixtures aren't fun. Machinist is.* 4 | 5 | - [Home page](http://github.com/notahat/machinist) 6 | - [Google group](http://groups.google.com/group/machinist-users), for support 7 | - [Bug tracker](http://github.com/notahat/machinist/issues), for reporting Machinist bugs 8 | 9 | If you want Machinist 1, [go here](http://github.com/notahat/machinist/tree/1.0-maintenance). 10 | 11 | 12 | ## Introduction 13 | 14 | *Note: Machinist isn't under active development. See the Status section below for more info.* 15 | 16 | Machinist makes it easy to create objects for use in tests. It generates data 17 | for the attributes you don't care about, and constructs any necessary 18 | associated objects, leaving you to specify only the fields you care about in 19 | your test. For example: 20 | 21 | describe Comment, "without_spam scope" do 22 | it "doesn't include spam" do 23 | # This will make a Comment, a Post, and a User (the author of the 24 | # Post), generate values for all their attributes, and save them: 25 | spam = Comment.make!(:spam => true) 26 | 27 | Comment.without_spam.should_not include(spam) 28 | end 29 | end 30 | 31 | You tell Machinist how to do this with blueprints: 32 | 33 | require 'machinist/active_record' 34 | 35 | User.blueprint do 36 | username { "user#{sn}" } # Each user gets a unique serial number. 37 | end 38 | 39 | Post.blueprint do 40 | author 41 | title { "Post #{sn}" } 42 | body { "Lorem ipsum..." } 43 | end 44 | 45 | Comment.blueprint do 46 | post 47 | email { "commenter#{sn}@example.com" } 48 | body { "Lorem ipsum..." } 49 | end 50 | 51 | 52 | ## Installation 53 | 54 | ### Upgrading from Machinist 1 55 | 56 | See [the wiki](http://wiki.github.com/notahat/machinist/machinist-2). 57 | 58 | ### Rails 3 59 | 60 | In your app's `Gemfile`, in the `group :test` section, add: 61 | 62 | gem 'machinist', '>= 2.0.0.beta2' 63 | 64 | Then run: 65 | 66 | bundle 67 | rails generate machinist:install 68 | 69 | If you want Machinist to automatically add a blueprint to your blueprints file 70 | whenever you generate a model, add the following to your `config/application.rb` 71 | inside the Application class: 72 | 73 | config.generators do |g| 74 | g.fixture_replacement :machinist 75 | end 76 | 77 | ### Rails 2 78 | 79 | See [the wiki](http://wiki.github.com/notahat/machinist/rails-2). 80 | 81 | 82 | ## Usage 83 | 84 | ### Blueprints 85 | 86 | A blueprint describes how to generate an object. The blueprint takes care of 87 | providing attributes that your test doesn't care about, leaving you to focus on 88 | just the attributes that are important for the test. 89 | 90 | A simple blueprint might look like this: 91 | 92 | Post.blueprint do 93 | title { "A Post" } 94 | body { "Lorem ipsum..." } 95 | end 96 | 97 | You can then construct a Post from this blueprint with: 98 | 99 | Post.make! 100 | 101 | When you call `make!`, Machinist calls `Post.new`, then runs through the 102 | attributes in your blueprint, calling the block for each attribute to generate 103 | a value. It then saves and reloads the Post. (It throws an exception if the 104 | Post can't be saved.) 105 | 106 | You can override values defined in the blueprint by passing a hash to make: 107 | 108 | Post.make!(:title => "A Specific Title") 109 | 110 | If you want to generate an object without saving it to the database, replace 111 | `make!` with `make`. 112 | 113 | 114 | ### Unique Attributes 115 | 116 | For attributes that need to be unique, you can call the `sn` method from 117 | within the attribute block to get a unique serial number for the object. 118 | 119 | User.blueprint do 120 | username { "user-#{sn}" } 121 | end 122 | 123 | 124 | ### Associations 125 | 126 | If your object needs associated objects, you can generate them like this: 127 | 128 | Comment.blueprint do 129 | post { Post.make } 130 | end 131 | 132 | Calling `Comment.make!` will construct a Comment and its associated Post, and 133 | save both. 134 | 135 | Machinist is smart enough to look at the association and work out what sort of 136 | object it needs to create, so you can shorten the above blueprint to: 137 | 138 | Comment.blueprint do 139 | post 140 | end 141 | 142 | If you want to override the value for post when constructing the comment, you 143 | can do this: 144 | 145 | post = Post.make(:title => "A particular title) 146 | comment = Comment.make(:post => post) 147 | 148 | 149 | For `has_many` and `has_and_belongs_to_many` associations, you can create 150 | multiple associated objects like this: 151 | 152 | Post.blueprint do 153 | comments(3) # Makes 3 comments. 154 | end 155 | 156 | 157 | ### Named Blueprints 158 | 159 | Named blueprints let you define variations on an object. For example, suppose 160 | some of your Users are administrators: 161 | 162 | User.blueprint do 163 | name { "User #{sn}" } 164 | email { "user-#{sn}@example.com" } 165 | end 166 | 167 | User.blueprint(:admin) do 168 | name { "Admin User #{sn}" } 169 | admin { true } 170 | end 171 | 172 | Calling: 173 | 174 | User.make!(:admin) 175 | 176 | will use the `:admin` blueprint. 177 | 178 | Named blueprints call the default blueprint to set any attributes not 179 | specifically provided, so in this example the `email` attribute will still be 180 | generated even for an admin user. 181 | 182 | You must define a default blueprint for any class that has a named blueprint, 183 | even if the default blueprint is empty. 184 | 185 | 186 | ### Blueprints on Plain Old Ruby Objects 187 | 188 | Machinist also works with plain old Ruby objects. Let's say you have a class like: 189 | 190 | class Post 191 | extend Machinist::Machinable 192 | 193 | attr_accessor :title 194 | attr_accessor :body 195 | end 196 | 197 | You can blueprint the Post class just like anything else: 198 | 199 | Post.blueprint do 200 | title { "A title!" } 201 | body { "A body!" } 202 | end 203 | 204 | And `Post.make` will construct a new Post. 205 | 206 | 207 | ### Other Tricks 208 | 209 | You can refer to already assigned attributes when constructing a new attribute: 210 | 211 | Post.blueprint do 212 | author { "Author #{sn}" } 213 | body { "Post by #{object.author}" } 214 | end 215 | 216 | 217 | ### More Details 218 | 219 | Read the code! No, really. I wrote this code to be read. 220 | 221 | Check out [the specs](https://github.com/notahat/machinist/tree/master/spec), 222 | starting with [the spec for 223 | Machinable](https://github.com/notahat/machinist/blob/master/spec/machinable_spec.rb). 224 | 225 | 226 | ## Compatibility 227 | 228 | I've tested this with: 229 | 230 | Ruby versions: 1.8.7, 1.9.2, 1.9.3, 2.0.0 231 | Rails versions: 2.3, 3.0, 3.2 232 | 233 | It may well be happy with other versions too, but I'm not promising anything. 234 | Compatibility patches are welcome. 235 | 236 | 237 | ## Developing 238 | 239 | The Machinist specs and source code were written to be read, and I'm pretty 240 | happy with them. Don't be afraid to have a look under the hood! 241 | 242 | If you want to submit a patch: 243 | 244 | - Fork the project. 245 | - Make your feature addition or bug fix. 246 | - Add tests for it. This is important so I don't break it in a 247 | future version unintentionally. 248 | - Commit, do not mess with rakefile, version, or history. 249 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 250 | - Send me a pull request. Bonus points for topic branches. 251 | 252 | 253 | ## Status 254 | 255 | In active use in a number of large Rails 2 and 3 apps. 256 | 257 | Development is sporadic at best, as I find myself with less and less need for 258 | factories in tests. See Bo Jeanes' 259 | [excellent article on the topic](http://bjeanes.com/2012/02/factories-breed-complexity). 260 | 261 | If anybody wants to take over maintenance, let me know. 262 | 263 | 264 | ## Contributors 265 | 266 | Machinist is maintained by Pete Yandell ([pete@notahat.com](mailto:pete@notahat.com), [@notahat](http://twitter.com/notahat)) 267 | 268 | Other contributors include: 269 | 270 | [Marcos Arias](http://github.com/yizzreel), 271 | [Jack Dempsey](http://github.com/jackdempsey), 272 | [Jeremy Durham](http://github.com/jeremydurham), 273 | [Clinton Forbes](http://github.com/clinton), 274 | [Perryn Fowler](http://github.com/perryn), 275 | [Niels Ganser](http://github.com/Nielsomat), 276 | [Jeremy Grant](http://github.com/jeremygrant), 277 | [Jon Guymon](http://github.com/gnarg), 278 | [James Healy](http://github.com/yob), 279 | [Ben Hoskings](http://github.com/benhoskings), 280 | [Evan David Light](http://github.com/elight), 281 | [Chris Lloyd](http://github.com/chrislloyd), 282 | [Adam Meehan](http://github.com/adzap), 283 | [Kyle Neath](http://github.com/kneath), 284 | [Lawrence Pit](http://github.com/lawrencepit), 285 | [Xavier Shay](http://github.com/xaviershay), 286 | [T.J. Sheehy](http://github.com/tjsheehy), 287 | [Roland Swingler](http://github.com/knaveofdiamonds), 288 | [Gareth Townsend](http://github.com/quamen), 289 | [Matt Wastrodowski](http://github.com/towski), 290 | [Ian White](http://github.com/ianwhite) 291 | 292 | Thanks to Thoughtbot's [Factory 293 | Girl](http://github.com/thoughtbot/factory_girl/tree/master). Machinist was 294 | written because I loved the idea behind Factory Girl, but I thought the 295 | philosophy wasn't quite right, and I hated the syntax. 296 | 297 | --------------------------------------------------------------------------------