indirect-machinist 2.0.0.beta3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,7 @@
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 -%>
@@ -0,0 +1,13 @@
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
+
@@ -0,0 +1,5 @@
1
+ require 'machinist/blueprint'
2
+ require 'machinist/exceptions'
3
+ require 'machinist/lathe'
4
+ require 'machinist/machinable'
5
+ require 'machinist/railtie' if defined?(Rails::Railtie)
@@ -0,0 +1,14 @@
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
@@ -0,0 +1,16 @@
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
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,89 @@
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
+ @block = block
16
+ end
17
+
18
+ attr_reader :klass, :parent, :block
19
+
20
+ # Generate an object from this blueprint.
21
+ #
22
+ # Pass in attributes to override values defined in the blueprint.
23
+ def make(attributes = {})
24
+ lathe = lathe_class.new(@klass, new_serial_number, attributes)
25
+
26
+ lathe.instance_eval(&@block)
27
+ each_ancestor {|blueprint| lathe.instance_eval(&blueprint.block) }
28
+
29
+ lathe.object
30
+ end
31
+
32
+ # Returns the Lathe class used to make objects for this blueprint.
33
+ #
34
+ # Subclasses can override this to substitute a custom lathe class.
35
+ def lathe_class
36
+ Lathe
37
+ end
38
+
39
+ # Returns the parent blueprint for this blueprint.
40
+ def parent_blueprint
41
+ case @parent
42
+ when nil
43
+ nil
44
+ when Blueprint
45
+ # @parent references the parent blueprint directly.
46
+ @parent
47
+ else
48
+ # @parent is a class in which we should look for a blueprint.
49
+ find_blueprint_in_superclass_chain(@parent)
50
+ end
51
+ end
52
+
53
+ # Yields the parent blueprint, its parent blueprint, etc.
54
+ def each_ancestor
55
+ ancestor = parent_blueprint
56
+ while ancestor
57
+ yield ancestor
58
+ ancestor = ancestor.parent_blueprint
59
+ end
60
+ end
61
+
62
+ protected
63
+
64
+ def new_serial_number #:nodoc:
65
+ parent_blueprint = self.parent_blueprint # Cache this for speed.
66
+ if parent_blueprint
67
+ parent_blueprint.new_serial_number
68
+ else
69
+ @serial_number ||= 0
70
+ @serial_number += 1
71
+ sprintf("%04d", @serial_number)
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def find_blueprint_in_superclass_chain(klass)
78
+ until has_blueprint?(klass) || klass.nil?
79
+ klass = klass.superclass
80
+ end
81
+ klass && klass.blueprint
82
+ end
83
+
84
+ def has_blueprint?(klass)
85
+ klass.respond_to?(:blueprint) && !klass.blueprint.nil?
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,68 @@
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, serial_number, attributes = {})
11
+ @klass = klass
12
+ @serial_number = serial_number
13
+ @assigned_attributes = {}
14
+
15
+ @object = @klass.new
16
+ attributes.each {|key, value| assign_attribute(key, value) }
17
+ end
18
+
19
+ # Returns a unique serial number for the object under construction.
20
+ def sn
21
+ @serial_number
22
+ end
23
+
24
+ # Returns the object under construction.
25
+ attr_reader :object
26
+
27
+ def method_missing(attribute, *args, &block) #:nodoc:
28
+ unless attribute_assigned?(attribute)
29
+ assign_attribute(attribute, make_attribute(attribute, args, &block))
30
+ end
31
+ end
32
+
33
+ # Undef a couple of methods that are common ActiveRecord attributes.
34
+ # (Both of these are deprecated in Ruby 1.8 anyway.)
35
+ undef_method :id if respond_to?(:id)
36
+ undef_method :type if respond_to?(:type)
37
+
38
+ protected
39
+
40
+ def make_attribute(attribute, args, &block) #:nodoc:
41
+ count = args.shift if args.first.is_a?(Fixnum)
42
+ if count
43
+ Array.new(count) { make_one_value(attribute, args, &block) }
44
+ else
45
+ make_one_value(attribute, args, &block)
46
+ end
47
+ end
48
+
49
+ def make_one_value(attribute, args) #:nodoc:
50
+ raise_argument_error(attribute) unless args.empty?
51
+ yield
52
+ end
53
+
54
+ def assign_attribute(key, value) #:nodoc:
55
+ @assigned_attributes[key.to_sym] = value
56
+ @object.send("#{key}=", value)
57
+ end
58
+
59
+ def attribute_assigned?(key) #:nodoc:
60
+ @assigned_attributes.has_key?(key.to_sym)
61
+ end
62
+
63
+ def raise_argument_error(attribute) #:nodoc:
64
+ raise ArgumentError.new("Invalid arguments to attribute #{attribute} in blueprint")
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,95 @@
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(name = :master, &block)
16
+ @blueprints ||= {}
17
+ if block_given?
18
+ parent = (name == :master ? superclass : self) # Where should we look for the parent blueprint?
19
+ @blueprints[name] = blueprint_class.new(self, :parent => parent, &block)
20
+ end
21
+ @blueprints[name]
22
+ end
23
+
24
+ # Construct an object from a blueprint.
25
+ #
26
+ # :call-seq:
27
+ # make([count], [blueprint_name], [attributes = {}])
28
+ #
29
+ # [+count+]
30
+ # The number of objects to construct. If +count+ is provided, make
31
+ # returns an array of objects rather than a single object.
32
+ # [+blueprint_name+]
33
+ # Construct the object from the named blueprint, rather than the master
34
+ # blueprint.
35
+ # [+attributes+]
36
+ # Override the attributes from the blueprint with values from this hash.
37
+ def make(*args)
38
+ decode_args_to_make(*args) do |blueprint, attributes|
39
+ blueprint.make(attributes)
40
+ end
41
+ end
42
+
43
+ # Construct and save an object from a blueprint, if the class allows saving.
44
+ #
45
+ # :call-seq:
46
+ # make!([count], [blueprint_name], [attributes = {}])
47
+ #
48
+ # Arguments are the same as for make.
49
+ def make!(*args)
50
+ decode_args_to_make(*args) do |blueprint, attributes|
51
+ raise BlueprintCantSaveError.new(blueprint) unless blueprint.respond_to?(:make!)
52
+ blueprint.make!(attributes)
53
+ end
54
+ end
55
+
56
+ # Remove all blueprints defined on this class.
57
+ def clear_blueprints!
58
+ @blueprints = {}
59
+ end
60
+
61
+ # Classes that include Machinable can override this method if they want to
62
+ # use a custom blueprint class when constructing blueprints.
63
+ #
64
+ # The default is Machinist::Blueprint.
65
+ def blueprint_class
66
+ Machinist::Blueprint
67
+ end
68
+
69
+ private
70
+
71
+ # Parses the arguments to make.
72
+ #
73
+ # Yields a blueprint and an attributes hash to the block, which should
74
+ # construct an object from them. The block may be called multiple times to
75
+ # construct multiple objects.
76
+ def decode_args_to_make(*args) #:nodoc:
77
+ shift_arg = lambda {|klass| args.shift if args.first.is_a?(klass) }
78
+ count = shift_arg[Fixnum]
79
+ name = shift_arg[Symbol] || :master
80
+ attributes = shift_arg[Hash] || {}
81
+ raise ArgumentError.new("Couldn't understand arguments") unless args.empty?
82
+
83
+ @blueprints ||= {}
84
+ blueprint = @blueprints[name]
85
+ raise NoBlueprintError.new(self, name) unless blueprint
86
+
87
+ if count.nil?
88
+ yield(blueprint, attributes)
89
+ else
90
+ Array.new(count) { yield(blueprint, attributes) }
91
+ end
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,5 @@
1
+ module Machinist
2
+ class Railtie < ::Rails::Railtie
3
+ config.generators.fixture_replacement :machinist
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Machinist
2
+ VERSION = "2.0.0.beta3"
3
+ end
@@ -0,0 +1,27 @@
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 = "indirect-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.rubyforge_project = "machinist"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "activerecord"
22
+ s.add_development_dependency "mysql"
23
+ s.add_development_dependency "rake"
24
+ s.add_development_dependency "rcov"
25
+ s.add_development_dependency "rspec"
26
+ s.add_development_dependency "rdoc"
27
+ end
@@ -0,0 +1,108 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'support/active_record_environment'
3
+
4
+ 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
+ post.should be_a(Post)
16
+ post.should 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
+ post.should be_a(Post)
25
+ post.should_not be_new_record
26
+ end
27
+
28
+ it "raises an exception for an invalid object" do
29
+ User.blueprint { }
30
+ lambda {
31
+ User.make!(:username => "")
32
+ }.should 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
+ post.should be_a(Post)
46
+ post.should_not be_new_record
47
+ post.author.should be_a(User)
48
+ post.author.should_not 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
+ post.should be_a(Post)
58
+ post.should_not be_new_record
59
+ post.should have(3).comments
60
+ post.comments.each do |comment|
61
+ comment.should be_a(Comment)
62
+ comment.should_not 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
+ post.should be_a(Post)
75
+ post.should_not be_new_record
76
+ post.should have(3).tags
77
+ post.tags.each do |tag|
78
+ tag.should be_a(Tag)
79
+ tag.should_not 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
+ post.should be_a(Post)
92
+ post.should_not be_new_record
93
+ post.author.should be_a(User)
94
+ post.author.should_not be_new_record
95
+ post.author.username.should =~ /^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
+ lambda {
103
+ User.make
104
+ }.should raise_error(ArgumentError)
105
+ end
106
+ end
107
+
108
+ end