machinist 1.0.3

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.
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
+