machinist 1.0.6 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +3 -2
  2. data/Gemfile +8 -0
  3. data/MIT-LICENSE +2 -1
  4. data/README.markdown +39 -271
  5. data/Rakefile +22 -14
  6. data/lib/generators/machinist/install/USAGE +2 -0
  7. data/lib/generators/machinist/install/install_generator.rb +48 -0
  8. data/lib/generators/machinist/install/templates/blueprints.rb +9 -0
  9. data/lib/generators/machinist/install/templates/machinist.rb.erb +10 -0
  10. data/lib/generators/machinist/model/model_generator.rb +13 -0
  11. data/lib/machinist.rb +11 -105
  12. data/lib/machinist/active_record.rb +8 -93
  13. data/lib/machinist/active_record/blueprint.rb +41 -0
  14. data/lib/machinist/active_record/lathe.rb +24 -0
  15. data/lib/machinist/blueprint.rb +89 -0
  16. data/lib/machinist/exceptions.rb +32 -0
  17. data/lib/machinist/lathe.rb +69 -0
  18. data/lib/machinist/machinable.rb +97 -0
  19. data/lib/machinist/shop.rb +52 -0
  20. data/lib/machinist/warehouse.rb +36 -0
  21. data/spec/active_record_spec.rb +100 -169
  22. data/spec/blueprint_spec.rb +74 -0
  23. data/spec/exceptions_spec.rb +20 -0
  24. data/spec/inheritance_spec.rb +104 -0
  25. data/spec/machinable_spec.rb +101 -0
  26. data/spec/shop_spec.rb +94 -0
  27. data/spec/spec_helper.rb +4 -6
  28. data/spec/support/active_record_environment.rb +65 -0
  29. data/spec/warehouse_spec.rb +24 -0
  30. metadata +52 -40
  31. data/.autotest +0 -7
  32. data/FAQ.markdown +0 -18
  33. data/VERSION +0 -1
  34. data/init.rb +0 -2
  35. data/lib/machinist/blueprints.rb +0 -25
  36. data/lib/machinist/data_mapper.rb +0 -83
  37. data/lib/machinist/object.rb +0 -30
  38. data/lib/machinist/sequel.rb +0 -62
  39. data/lib/sham.rb +0 -77
  40. data/machinist.gemspec +0 -72
  41. data/spec/data_mapper_spec.rb +0 -134
  42. data/spec/db/.gitignore +0 -1
  43. data/spec/db/schema.rb +0 -20
  44. data/spec/log/.gitignore +0 -1
  45. data/spec/machinist_spec.rb +0 -190
  46. data/spec/sequel_spec.rb +0 -146
  47. data/spec/sham_spec.rb +0 -95
@@ -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,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 '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) && !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
- 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 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
- @@nerfed = false
106
- def self.nerfed?
107
- @@nerfed
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 Machinist
6
-
7
- class ActiveRecordAdapter
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 plan(*args)
85
- lathe = Lathe.run(Machinist::ActiveRecordAdapter, self.build, *args)
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