machinist 1.0.6 → 2.0.0.beta1
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/.gitignore +3 -2
- data/Gemfile +8 -0
- data/MIT-LICENSE +2 -1
- data/README.markdown +39 -271
- data/Rakefile +22 -14
- data/lib/generators/machinist/install/USAGE +2 -0
- data/lib/generators/machinist/install/install_generator.rb +48 -0
- data/lib/generators/machinist/install/templates/blueprints.rb +9 -0
- data/lib/generators/machinist/install/templates/machinist.rb.erb +10 -0
- data/lib/generators/machinist/model/model_generator.rb +13 -0
- data/lib/machinist.rb +11 -105
- data/lib/machinist/active_record.rb +8 -93
- data/lib/machinist/active_record/blueprint.rb +41 -0
- data/lib/machinist/active_record/lathe.rb +24 -0
- data/lib/machinist/blueprint.rb +89 -0
- data/lib/machinist/exceptions.rb +32 -0
- data/lib/machinist/lathe.rb +69 -0
- data/lib/machinist/machinable.rb +97 -0
- data/lib/machinist/shop.rb +52 -0
- data/lib/machinist/warehouse.rb +36 -0
- data/spec/active_record_spec.rb +100 -169
- data/spec/blueprint_spec.rb +74 -0
- data/spec/exceptions_spec.rb +20 -0
- data/spec/inheritance_spec.rb +104 -0
- data/spec/machinable_spec.rb +101 -0
- data/spec/shop_spec.rb +94 -0
- data/spec/spec_helper.rb +4 -6
- data/spec/support/active_record_environment.rb +65 -0
- data/spec/warehouse_spec.rb +24 -0
- metadata +52 -40
- data/.autotest +0 -7
- data/FAQ.markdown +0 -18
- data/VERSION +0 -1
- data/init.rb +0 -2
- data/lib/machinist/blueprints.rb +0 -25
- data/lib/machinist/data_mapper.rb +0 -83
- data/lib/machinist/object.rb +0 -30
- data/lib/machinist/sequel.rb +0 -62
- data/lib/sham.rb +0 -77
- data/machinist.gemspec +0 -72
- data/spec/data_mapper_spec.rb +0 -134
- data/spec/db/.gitignore +0 -1
- data/spec/db/schema.rb +0 -20
- data/spec/log/.gitignore +0 -1
- data/spec/machinist_spec.rb +0 -190
- data/spec/sequel_spec.rb +0 -146
- data/spec/sham_spec.rb +0 -95
@@ -0,0 +1,10 @@
|
|
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 -%>
|
8
|
+
|
9
|
+
# Reset the Machinist cache before each scenario.
|
10
|
+
Before { Machinist.reset_before_test }
|
@@ -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
|
+
|
data/lib/machinist.rb
CHANGED
@@ -1,110 +1,16 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
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) && !nil_or_empty?(@object.send(symbol))
|
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
|
1
|
+
require 'machinist/blueprint'
|
2
|
+
require 'machinist/exceptions'
|
3
|
+
require 'machinist/lathe'
|
4
|
+
require 'machinist/machinable'
|
5
|
+
require 'machinist/shop'
|
6
|
+
require 'machinist/warehouse'
|
49
7
|
|
50
|
-
|
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 nil_or_empty?(object)
|
62
|
-
object.respond_to?(:empty?) ? object.empty? : object.nil?
|
63
|
-
end
|
64
|
-
|
65
|
-
def assign_attribute(key, value)
|
66
|
-
assigned_attributes[key.to_sym] = value
|
67
|
-
@object.send("#{key}=", value)
|
68
|
-
end
|
69
|
-
|
70
|
-
def attribute_assigned?(key)
|
71
|
-
assigned_attributes.has_key?(key.to_sym)
|
72
|
-
end
|
73
|
-
|
74
|
-
def generate_attribute_value(attribute, *args)
|
75
|
-
if block_given?
|
76
|
-
# If we've got a block, use that to generate the value.
|
77
|
-
yield
|
78
|
-
else
|
79
|
-
# Otherwise, look for an association or a sham.
|
80
|
-
if @adapter.has_association?(object, attribute)
|
81
|
-
@adapter.class_for_association(object, attribute).make(args.first || {})
|
82
|
-
elsif args.empty?
|
83
|
-
Sham.send(attribute)
|
84
|
-
else
|
85
|
-
# If we've got a constant, just use that.
|
86
|
-
args.first
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
end
|
92
|
-
|
93
|
-
# This sets a flag that stops make from saving objects, so
|
94
|
-
# that calls to make from within a blueprint don't create
|
95
|
-
# anything inside make_unsaved.
|
96
|
-
def self.with_save_nerfed
|
97
|
-
begin
|
98
|
-
@@nerfed = true
|
99
|
-
yield
|
100
|
-
ensure
|
101
|
-
@@nerfed = false
|
102
|
-
end
|
103
|
-
end
|
8
|
+
module Machinist
|
104
9
|
|
105
|
-
|
106
|
-
def self.
|
107
|
-
|
10
|
+
# Call this before each test to get Machinist ready.
|
11
|
+
def self.reset_before_test
|
12
|
+
Shop.instance.restock
|
108
13
|
end
|
109
14
|
|
110
15
|
end
|
16
|
+
|
@@ -1,99 +1,14 @@
|
|
1
|
-
require 'machinist'
|
2
|
-
require 'machinist/blueprints'
|
3
1
|
require 'active_record'
|
2
|
+
require 'machinist'
|
3
|
+
require 'machinist/active_record/blueprint'
|
4
|
+
require 'machinist/active_record/lathe'
|
4
5
|
|
5
|
-
module
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
def self.has_association?(object, attribute)
|
10
|
-
object.class.reflect_on_association(attribute)
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.class_for_association(object, attribute)
|
14
|
-
association = object.class.reflect_on_association(attribute)
|
15
|
-
association && association.klass
|
16
|
-
end
|
17
|
-
|
18
|
-
# This method takes care of converting any associated objects,
|
19
|
-
# in the hash returned by Lathe#assigned_attributes, into their
|
20
|
-
# object ids.
|
21
|
-
#
|
22
|
-
# For example, let's say we have blueprints like this:
|
23
|
-
#
|
24
|
-
# Post.blueprint { }
|
25
|
-
# Comment.blueprint { post }
|
26
|
-
#
|
27
|
-
# Lathe#assigned_attributes will return { :post => ... }, but
|
28
|
-
# we want to pass { :post_id => 1 } to a controller.
|
29
|
-
#
|
30
|
-
# This method takes care of cleaning this up.
|
31
|
-
def self.assigned_attributes_without_associations(lathe)
|
32
|
-
attributes = {}
|
33
|
-
lathe.assigned_attributes.each_pair do |attribute, value|
|
34
|
-
association = lathe.object.class.reflect_on_association(attribute)
|
35
|
-
if association && association.macro == :belongs_to && !value.nil?
|
36
|
-
attributes[association.primary_key_name.to_sym] = value.id
|
37
|
-
else
|
38
|
-
attributes[attribute] = value
|
39
|
-
end
|
40
|
-
end
|
41
|
-
attributes
|
42
|
-
end
|
43
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
module ActiveRecordExtensions
|
47
|
-
def self.included(base)
|
48
|
-
base.extend(ClassMethods)
|
49
|
-
end
|
50
|
-
|
51
|
-
module ClassMethods
|
52
|
-
def make(*args, &block)
|
53
|
-
lathe = Lathe.run(Machinist::ActiveRecordAdapter, self.new, *args)
|
54
|
-
unless Machinist.nerfed?
|
55
|
-
lathe.object.save!
|
56
|
-
lathe.object.reload
|
57
|
-
end
|
58
|
-
lathe.object(&block)
|
59
|
-
end
|
60
|
-
|
61
|
-
def make_unsaved(*args)
|
62
|
-
object = Machinist.with_save_nerfed { make(*args) }
|
63
|
-
yield object if block_given?
|
64
|
-
object
|
65
|
-
end
|
66
|
-
|
67
|
-
def plan(*args)
|
68
|
-
lathe = Lathe.run(Machinist::ActiveRecordAdapter, self.new, *args)
|
69
|
-
Machinist::ActiveRecordAdapter.assigned_attributes_without_associations(lathe)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
module ActiveRecordHasManyExtensions
|
75
|
-
def make(*args, &block)
|
76
|
-
lathe = Lathe.run(Machinist::ActiveRecordAdapter, self.build, *args)
|
77
|
-
unless Machinist.nerfed?
|
78
|
-
lathe.object.save!
|
79
|
-
lathe.object.reload
|
80
|
-
end
|
81
|
-
lathe.object(&block)
|
82
|
-
end
|
6
|
+
module ActiveRecord #:nodoc:
|
7
|
+
class Base #:nodoc:
|
8
|
+
extend Machinist::Machinable
|
83
9
|
|
84
|
-
def
|
85
|
-
|
86
|
-
Machinist::ActiveRecordAdapter.assigned_attributes_without_associations(lathe)
|
10
|
+
def self.blueprint_class
|
11
|
+
Machinist::ActiveRecord::Blueprint
|
87
12
|
end
|
88
13
|
end
|
89
|
-
|
90
|
-
end
|
91
|
-
|
92
|
-
class ActiveRecord::Base
|
93
|
-
include Machinist::Blueprints
|
94
|
-
include Machinist::ActiveRecordExtensions
|
95
|
-
end
|
96
|
-
|
97
|
-
class ActiveRecord::Associations::HasManyAssociation
|
98
|
-
include Machinist::ActiveRecordHasManyExtensions
|
99
14
|
end
|
@@ -0,0 +1,41 @@
|
|
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
|
+
# Box an object for storage in the warehouse.
|
12
|
+
def box(object)
|
13
|
+
object.id
|
14
|
+
end
|
15
|
+
|
16
|
+
# Unbox an object from the warehouse.
|
17
|
+
def unbox(id)
|
18
|
+
@klass.find(id)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Execute a block on a separate database connection, so that any database
|
22
|
+
# operations happen outside any open transactions.
|
23
|
+
def outside_transaction
|
24
|
+
# ActiveRecord manages connections per-thread, so the only way to
|
25
|
+
# convince it to open another connection is to start another thread.
|
26
|
+
thread = Thread.new do
|
27
|
+
begin
|
28
|
+
yield
|
29
|
+
ensure
|
30
|
+
::ActiveRecord::Base.connection_pool.checkin(::ActiveRecord::Base.connection)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
thread.value
|
34
|
+
end
|
35
|
+
|
36
|
+
def lathe_class #:nodoc:
|
37
|
+
Machinist::ActiveRecord::Lathe
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
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 do 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
|