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