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 +106 -0
- data/lib/machinist/active_record.rb +98 -0
- data/lib/machinist/blueprints.rb +25 -0
- data/lib/machinist/data_mapper.rb +83 -0
- data/lib/machinist/object.rb +30 -0
- data/lib/sham.rb +77 -0
- metadata +60 -0
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
|
+
|