machinist 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/machinist.rb ADDED
@@ -0,0 +1,106 @@
1
+ require 'sham'
2
+
3
+ module Machinist
4
+
5
+ # A Lathe is used to execute the blueprint and construct an object.
6
+ #
7
+ # The blueprint is instance_eval'd against the Lathe.
8
+ class Lathe
9
+ def self.run(adapter, object, *args)
10
+ blueprint = object.class.blueprint
11
+ named_blueprint = object.class.blueprint(args.shift) if args.first.is_a?(Symbol)
12
+ attributes = args.pop || {}
13
+
14
+ raise "No blueprint for class #{object.class}" if blueprint.nil?
15
+
16
+ lathe = self.new(adapter, object, attributes)
17
+ lathe.instance_eval(&named_blueprint) if named_blueprint
18
+ klass = object.class
19
+ while klass
20
+ lathe.instance_eval(&klass.blueprint) if klass.respond_to?(:blueprint) && klass.blueprint
21
+ klass = klass.superclass
22
+ end
23
+ lathe
24
+ end
25
+
26
+ def initialize(adapter, object, attributes = {})
27
+ @adapter = adapter
28
+ @object = object
29
+ attributes.each {|key, value| assign_attribute(key, value) }
30
+ end
31
+
32
+ def object
33
+ yield @object if block_given?
34
+ @object
35
+ end
36
+
37
+ def method_missing(symbol, *args, &block)
38
+ if attribute_assigned?(symbol)
39
+ # If we've already assigned the attribute, return that.
40
+ @object.send(symbol)
41
+ elsif @adapter.has_association?(@object, symbol) && !@object.send(symbol).nil?
42
+ # If the attribute is an association and is already assigned, return that.
43
+ @object.send(symbol)
44
+ else
45
+ # Otherwise generate a value and assign it.
46
+ assign_attribute(symbol, generate_attribute_value(symbol, *args, &block))
47
+ end
48
+ end
49
+
50
+ def assigned_attributes
51
+ @assigned_attributes ||= {}
52
+ end
53
+
54
+ # Undef a couple of methods that are common ActiveRecord attributes.
55
+ # (Both of these are deprecated in Ruby 1.8 anyway.)
56
+ undef_method :id if respond_to?(:id)
57
+ undef_method :type if respond_to?(:type)
58
+
59
+ private
60
+
61
+ def assign_attribute(key, value)
62
+ assigned_attributes[key.to_sym] = value
63
+ @object.send("#{key}=", value)
64
+ end
65
+
66
+ def attribute_assigned?(key)
67
+ assigned_attributes.has_key?(key.to_sym)
68
+ end
69
+
70
+ def generate_attribute_value(attribute, *args)
71
+ if block_given?
72
+ # If we've got a block, use that to generate the value.
73
+ yield
74
+ else
75
+ # Otherwise, look for an association or a sham.
76
+ if @adapter.has_association?(object, attribute)
77
+ @adapter.class_for_association(object, attribute).make(args.first || {})
78
+ elsif args.empty?
79
+ Sham.send(attribute)
80
+ else
81
+ # If we've got a constant, just use that.
82
+ args.first
83
+ end
84
+ end
85
+ end
86
+
87
+ end
88
+
89
+ # This sets a flag that stops make from saving objects, so
90
+ # that calls to make from within a blueprint don't create
91
+ # anything inside make_unsaved.
92
+ def self.with_save_nerfed
93
+ begin
94
+ @@nerfed = true
95
+ yield
96
+ ensure
97
+ @@nerfed = false
98
+ end
99
+ end
100
+
101
+ @@nerfed = false
102
+ def self.nerfed?
103
+ @@nerfed
104
+ end
105
+
106
+ end
@@ -0,0 +1,98 @@
1
+ require 'machinist'
2
+ require 'machinist/blueprints'
3
+
4
+ module Machinist
5
+
6
+ class ActiveRecordAdapter
7
+
8
+ def self.has_association?(object, attribute)
9
+ object.class.reflect_on_association(attribute)
10
+ end
11
+
12
+ def self.class_for_association(object, attribute)
13
+ association = object.class.reflect_on_association(attribute)
14
+ association && association.klass
15
+ end
16
+
17
+ # This method takes care of converting any associated objects,
18
+ # in the hash returned by Lathe#assigned_attributes, into their
19
+ # object ids.
20
+ #
21
+ # For example, let's say we have blueprints like this:
22
+ #
23
+ # Post.blueprint { }
24
+ # Comment.blueprint { post }
25
+ #
26
+ # Lathe#assigned_attributes will return { :post => ... }, but
27
+ # we want to pass { :post_id => 1 } to a controller.
28
+ #
29
+ # This method takes care of cleaning this up.
30
+ def self.assigned_attributes_without_associations(lathe)
31
+ attributes = {}
32
+ lathe.assigned_attributes.each_pair do |attribute, value|
33
+ association = lathe.object.class.reflect_on_association(attribute)
34
+ if association && association.macro == :belongs_to
35
+ attributes[association.primary_key_name.to_sym] = value.id
36
+ else
37
+ attributes[attribute] = value
38
+ end
39
+ end
40
+ attributes
41
+ end
42
+
43
+ end
44
+
45
+ module ActiveRecordExtensions
46
+ def self.included(base)
47
+ base.extend(ClassMethods)
48
+ end
49
+
50
+ module ClassMethods
51
+ def make(*args, &block)
52
+ lathe = Lathe.run(Machinist::ActiveRecordAdapter, self.new, *args)
53
+ unless Machinist.nerfed?
54
+ lathe.object.save!
55
+ lathe.object.reload
56
+ end
57
+ lathe.object(&block)
58
+ end
59
+
60
+ def make_unsaved(*args)
61
+ object = Machinist.with_save_nerfed { make(*args) }
62
+ yield object if block_given?
63
+ object
64
+ end
65
+
66
+ def plan(*args)
67
+ lathe = Lathe.run(Machinist::ActiveRecordAdapter, self.new, *args)
68
+ Machinist::ActiveRecordAdapter.assigned_attributes_without_associations(lathe)
69
+ end
70
+ end
71
+ end
72
+
73
+ module ActiveRecordHasManyExtensions
74
+ def make(*args, &block)
75
+ lathe = Lathe.run(Machinist::ActiveRecordAdapter, self.build, *args)
76
+ unless Machinist.nerfed?
77
+ lathe.object.save!
78
+ lathe.object.reload
79
+ end
80
+ lathe.object(&block)
81
+ end
82
+
83
+ def plan(*args)
84
+ lathe = Lathe.run(Machinist::ActiveRecordAdapter, self.build, *args)
85
+ Machinist::ActiveRecordAdapter.assigned_attributes_without_associations(lathe)
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ class ActiveRecord::Base
92
+ include Machinist::Blueprints
93
+ include Machinist::ActiveRecordExtensions
94
+ end
95
+
96
+ class ActiveRecord::Associations::HasManyAssociation
97
+ include Machinist::ActiveRecordHasManyExtensions
98
+ end
@@ -0,0 +1,25 @@
1
+ module Machinist
2
+ # Include this in a class to allow defining blueprints for that class.
3
+ module Blueprints
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def blueprint(name = :master, &blueprint)
10
+ @blueprints ||= {}
11
+ @blueprints[name] = blueprint if block_given?
12
+ @blueprints[name]
13
+ end
14
+
15
+ def named_blueprints
16
+ @blueprints.reject{|name,_| name == :master }.keys
17
+ end
18
+
19
+ def clear_blueprints!
20
+ @blueprints = {}
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,83 @@
1
+ require 'machinist'
2
+ require 'machinist/blueprints'
3
+ require 'dm-core'
4
+
5
+ module Machinist
6
+
7
+ class DataMapperAdapter
8
+ def self.has_association?(object, attribute)
9
+ object.class.relationships.has_key?(attribute)
10
+ end
11
+
12
+ def self.class_for_association(object, attribute)
13
+ association = object.class.relationships[attribute]
14
+ association && association.parent_model
15
+ end
16
+
17
+ def self.association_is_many_to_one?(association)
18
+ if defined?(DataMapper::Associations::ManyToOne::Relationship)
19
+ # We're using the next branch of DM
20
+ association.class == DataMapper::Associations::ManyToOne::Relationship
21
+ else
22
+ # We're using the 0.9 or less branch.
23
+ association.options[:max].nil?
24
+ end
25
+ end
26
+
27
+ # This method takes care of converting any associated objects,
28
+ # in the hash returned by Lathe#assigned_attributes, into their
29
+ # object ids.
30
+ #
31
+ # For example, let's say we have blueprints like this:
32
+ #
33
+ # Post.blueprint { }
34
+ # Comment.blueprint { post }
35
+ #
36
+ # Lathe#assigned_attributes will return { :post => ... }, but
37
+ # we want to pass { :post_id => 1 } to a controller.
38
+ #
39
+ # This method takes care of cleaning this up.
40
+ def self.assigned_attributes_without_associations(lathe)
41
+ attributes = {}
42
+ lathe.assigned_attributes.each_pair do |attribute, value|
43
+ association = lathe.object.class.relationships[attribute]
44
+ if association && association_is_many_to_one?(association)
45
+ # DataMapper child_key can have more than one property, but I'm not
46
+ # sure in what circumstances this would be the case. I'm assuming
47
+ # here that there's only one property.
48
+ key = association.child_key.map(&:field).first.to_sym
49
+ attributes[key] = value.id
50
+ else
51
+ attributes[attribute] = value
52
+ end
53
+ end
54
+ attributes
55
+ end
56
+ end
57
+
58
+ module DataMapperExtensions
59
+ def make(*args, &block)
60
+ lathe = Lathe.run(Machinist::DataMapperAdapter, self.new, *args)
61
+ unless Machinist.nerfed?
62
+ lathe.object.save || raise("Save failed")
63
+ lathe.object.reload
64
+ end
65
+ lathe.object(&block)
66
+ end
67
+
68
+ def make_unsaved(*args)
69
+ object = Machinist.with_save_nerfed { make(*args) }
70
+ yield object if block_given?
71
+ object
72
+ end
73
+
74
+ def plan(*args)
75
+ lathe = Lathe.run(Machinist::DataMapperAdapter, self.new, *args)
76
+ Machinist::DataMapperAdapter.assigned_attributes_without_associations(lathe)
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ DataMapper::Model.append_extensions(Machinist::Blueprints::ClassMethods)
83
+ DataMapper::Model.append_extensions(Machinist::DataMapperExtensions)
@@ -0,0 +1,30 @@
1
+ require 'machinist'
2
+ require 'machinist/blueprints'
3
+
4
+ module Machinist
5
+
6
+ module ObjectExtensions
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def make(*args, &block)
13
+ lathe = Lathe.run(Machinist::ObjectAdapter, self.new, *args)
14
+ lathe.object(&block)
15
+ end
16
+ end
17
+ end
18
+
19
+ class ObjectAdapter
20
+ def self.has_association?(object, attribute)
21
+ false
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ class Object
28
+ include Machinist::Blueprints
29
+ include Machinist::ObjectExtensions
30
+ end
data/lib/sham.rb ADDED
@@ -0,0 +1,77 @@
1
+ class Sham
2
+ @@shams = {}
3
+
4
+ # Over-ride module's built-in name method, so we can re-use it for
5
+ # generating names. This is a bit of a no-no, but we get away with
6
+ # it in this context.
7
+ def self.name(*args, &block)
8
+ method_missing(:name, *args, &block)
9
+ end
10
+
11
+ def self.method_missing(symbol, *args, &block)
12
+ if block_given?
13
+ @@shams[symbol] = Sham.new(symbol, args.pop || {}, &block)
14
+ else
15
+ sham = @@shams[symbol]
16
+ raise "No sham defined for #{symbol}" if sham.nil?
17
+ sham.fetch_value
18
+ end
19
+ end
20
+
21
+ def self.clear
22
+ @@shams = {}
23
+ end
24
+
25
+ def self.reset(scope = :before_all)
26
+ @@shams.values.each { |sham| sham.reset(scope) }
27
+ end
28
+
29
+ def self.define(&block)
30
+ Sham.instance_eval(&block)
31
+ end
32
+
33
+ def initialize(name, options = {}, &block)
34
+ @name = name
35
+ @generator = block
36
+ @offset = 0
37
+ @unique = options.has_key?(:unique) ? options[:unique] : true
38
+ generate_values(12)
39
+ end
40
+
41
+ def reset(scope)
42
+ if scope == :before_all
43
+ @offset, @before_offset = 0, nil
44
+ elsif @before_offset
45
+ @offset = @before_offset
46
+ else
47
+ @before_offset = @offset
48
+ end
49
+ end
50
+
51
+ def fetch_value
52
+ # Generate more values if we need them.
53
+ if @offset >= @values.length
54
+ generate_values(2 * @values.length)
55
+ raise "Can't generate more unique values for Sham.#{@name}" if @offset >= @values.length
56
+ end
57
+ result = @values[@offset]
58
+ @offset += 1
59
+ result
60
+ end
61
+
62
+ private
63
+
64
+ def generate_values(count)
65
+ @values = seeded { (1..count).map(&@generator) }
66
+ @values.uniq! if @unique
67
+ end
68
+
69
+ def seeded
70
+ begin
71
+ srand(1)
72
+ yield
73
+ ensure
74
+ srand
75
+ end
76
+ end
77
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: machinist
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Pete Yandell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-09 00:00:00 +11:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: pete@nothat.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/machinist.rb
26
+ - lib/sham.rb
27
+ - lib/machinist/blueprints.rb
28
+ - lib/machinist/object.rb
29
+ - lib/machinist/active_record.rb
30
+ - lib/machinist/data_mapper.rb
31
+ has_rdoc: true
32
+ homepage: http://github.com/notahat/machinist
33
+ licenses: []
34
+
35
+ post_install_message:
36
+ rdoc_options: []
37
+
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ requirements: []
53
+
54
+ rubyforge_project:
55
+ rubygems_version: 1.3.5
56
+ signing_key:
57
+ specification_version: 3
58
+ summary: Fixtures aren't fun. Machinist is.
59
+ test_files: []
60
+