indirect-machinist 2.0.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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