machinist 1.0.6 → 2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +7 -4
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +47 -0
  4. data/MIT-LICENSE +2 -1
  5. data/README.markdown +182 -204
  6. data/Rakefile +22 -29
  7. data/VERSION +1 -1
  8. data/lib/generators/machinist/install/USAGE +2 -0
  9. data/lib/generators/machinist/install/install_generator.rb +46 -0
  10. data/lib/generators/machinist/install/templates/blueprints.rb +9 -0
  11. data/lib/generators/machinist/install/templates/machinist.rb.erb +7 -0
  12. data/lib/generators/machinist/model/model_generator.rb +13 -0
  13. data/lib/machinist/active_record/blueprint.rb +16 -0
  14. data/lib/machinist/active_record/lathe.rb +24 -0
  15. data/lib/machinist/active_record.rb +8 -93
  16. data/lib/machinist/blueprint.rb +89 -0
  17. data/lib/machinist/exceptions.rb +32 -0
  18. data/lib/machinist/lathe.rb +68 -0
  19. data/lib/machinist/machinable.rb +95 -0
  20. data/lib/machinist/version.rb +3 -0
  21. data/lib/machinist.rb +4 -109
  22. data/machinist.gemspec +20 -65
  23. data/spec/active_record_spec.rb +85 -171
  24. data/spec/blueprint_spec.rb +76 -0
  25. data/spec/exceptions_spec.rb +20 -0
  26. data/spec/inheritance_spec.rb +104 -0
  27. data/spec/machinable_spec.rb +101 -0
  28. data/spec/spec_helper.rb +4 -6
  29. data/spec/support/active_record_environment.rb +65 -0
  30. metadata +125 -37
  31. data/.autotest +0 -7
  32. data/FAQ.markdown +0 -18
  33. data/init.rb +0 -2
  34. data/lib/machinist/blueprints.rb +0 -25
  35. data/lib/machinist/data_mapper.rb +0 -83
  36. data/lib/machinist/object.rb +0 -30
  37. data/lib/machinist/sequel.rb +0 -62
  38. data/lib/sham.rb +0 -77
  39. data/spec/data_mapper_spec.rb +0 -134
  40. data/spec/db/.gitignore +0 -1
  41. data/spec/db/schema.rb +0 -20
  42. data/spec/log/.gitignore +0 -1
  43. data/spec/machinist_spec.rb +0 -190
  44. data/spec/sequel_spec.rb +0 -146
  45. data/spec/sham_spec.rb +0 -95
@@ -0,0 +1,46 @@
1
+ module Machinist
2
+ module Generators #:nodoc:
3
+ class InstallGenerator < Rails::Generators::Base #:nodoc:
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ class_option :test_framework, :type => :string, :aliases => "-t", :desc => "Test framework to use Machinist with"
7
+ class_option :cucumber, :type => :boolean, :desc => "Set up access to Machinist from Cucumber"
8
+
9
+ def blueprints_file
10
+ if rspec?
11
+ copy_file "blueprints.rb", "spec/support/blueprints.rb"
12
+ else
13
+ copy_file "blueprints.rb", "test/blueprints.rb"
14
+ end
15
+ end
16
+
17
+ def test_helper
18
+ if test_unit?
19
+ inject_into_file("test/test_helper.rb", :after => "require 'rails/test_help'\n") do
20
+ "require File.expand_path(File.dirname(__FILE__) + '/blueprints')\n"
21
+ end
22
+ end
23
+ end
24
+
25
+ def cucumber_support
26
+ if cucumber?
27
+ template "machinist.rb.erb", "features/support/machinist.rb"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def rspec?
34
+ options[:test_framework].to_sym == :rspec
35
+ end
36
+
37
+ def test_unit?
38
+ options[:test_framework].to_sym == :test_unit
39
+ end
40
+
41
+ def cucumber?
42
+ options[:cucumber]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -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,7 @@
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 -%>
@@ -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
+
@@ -0,0 +1,16 @@
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
+ def lathe_class #:nodoc:
12
+ Machinist::ActiveRecord::Lathe
13
+ end
14
+
15
+ end
16
+ 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
@@ -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,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 to 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
@@ -0,0 +1,68 @@
1
+ module Machinist
2
+
3
+ # When you make an object, the blueprint for that object is instance-evaled
4
+ # against a Lathe.
5
+ #
6
+ # The Lathe implements all the methods that are available to the blueprint,
7
+ # including method_missing to let the blueprint define attributes.
8
+ class Lathe
9
+
10
+ def initialize(klass, serial_number, attributes = {})
11
+ @klass = klass
12
+ @serial_number = serial_number
13
+ @assigned_attributes = {}
14
+
15
+ @object = @klass.new
16
+ attributes.each {|key, value| assign_attribute(key, value) }
17
+ end
18
+
19
+ # Returns a unique serial number for the object under construction.
20
+ def sn
21
+ @serial_number
22
+ end
23
+
24
+ # Returns the object under construction.
25
+ attr_reader :object
26
+
27
+ def method_missing(attribute, *args, &block) #:nodoc:
28
+ unless attribute_assigned?(attribute)
29
+ assign_attribute(attribute, make_attribute(attribute, args, &block))
30
+ end
31
+ end
32
+
33
+ # Undef a couple of methods that are common ActiveRecord attributes.
34
+ # (Both of these are deprecated in Ruby 1.8 anyway.)
35
+ undef_method :id if respond_to?(:id)
36
+ undef_method :type if respond_to?(:type)
37
+
38
+ protected
39
+
40
+ def make_attribute(attribute, args, &block) #:nodoc:
41
+ count = args.shift if args.first.is_a?(Fixnum)
42
+ if count
43
+ Array.new(count) { make_one_value(attribute, args, &block) }
44
+ else
45
+ make_one_value(attribute, args, &block)
46
+ end
47
+ end
48
+
49
+ def make_one_value(attribute, args) #:nodoc:
50
+ raise_argument_error(attribute) unless args.empty?
51
+ yield
52
+ end
53
+
54
+ def assign_attribute(key, value) #:nodoc:
55
+ @assigned_attributes[key.to_sym] = value
56
+ @object.send("#{key}=", value)
57
+ end
58
+
59
+ def attribute_assigned?(key) #:nodoc:
60
+ @assigned_attributes.has_key?(key.to_sym)
61
+ end
62
+
63
+ def raise_argument_error(attribute) #:nodoc:
64
+ raise ArgumentError.new("Invalid arguments to attribute #{attribute} in blueprint")
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,95 @@
1
+ module Machinist
2
+
3
+ # Extend classes with this module to define the blueprint and make methods.
4
+ module Machinable
5
+ # Define a blueprint with the given name for this class.
6
+ #
7
+ # e.g.
8
+ # Post.blueprint do
9
+ # title { "A Post" }
10
+ # body { "Lorem ipsum..." }
11
+ # end
12
+ #
13
+ # If you provide the +name+ argument, a named blueprint will be created.
14
+ # See the +blueprint_name+ argument to the make method.
15
+ def blueprint(name = :master, &block)
16
+ @blueprints ||= {}
17
+ if block_given?
18
+ parent = (name == :master ? superclass : self) # Where should we look for the parent blueprint?
19
+ @blueprints[name] = blueprint_class.new(self, :parent => parent, &block)
20
+ end
21
+ @blueprints[name]
22
+ end
23
+
24
+ # Construct an object from a blueprint.
25
+ #
26
+ # :call-seq:
27
+ # make([count], [blueprint_name], [attributes = {}])
28
+ #
29
+ # [+count+]
30
+ # The number of objects to construct. If +count+ is provided, make
31
+ # returns an array of objects rather than a single object.
32
+ # [+blueprint_name+]
33
+ # Construct the object from the named blueprint, rather than the master
34
+ # blueprint.
35
+ # [+attributes+]
36
+ # Override the attributes from the blueprint with values from this hash.
37
+ def make(*args)
38
+ decode_args_to_make(*args) do |blueprint, attributes|
39
+ blueprint.make(attributes)
40
+ end
41
+ end
42
+
43
+ # Construct and save an object from a blueprint, if the class allows saving.
44
+ #
45
+ # :call-seq:
46
+ # make!([count], [blueprint_name], [attributes = {}])
47
+ #
48
+ # Arguments are the same as for make.
49
+ def make!(*args)
50
+ decode_args_to_make(*args) do |blueprint, attributes|
51
+ raise BlueprintCantSaveError.new(blueprint) unless blueprint.respond_to?(:make!)
52
+ blueprint.make!(attributes)
53
+ end
54
+ end
55
+
56
+ # Remove all blueprints defined on this class.
57
+ def clear_blueprints!
58
+ @blueprints = {}
59
+ end
60
+
61
+ # Classes that include Machinable can override this method if they want to
62
+ # use a custom blueprint class when constructing blueprints.
63
+ #
64
+ # The default is Machinist::Blueprint.
65
+ def blueprint_class
66
+ Machinist::Blueprint
67
+ end
68
+
69
+ private
70
+
71
+ # Parses the arguments to make.
72
+ #
73
+ # Yields a blueprint and an attributes hash to the block, which should
74
+ # construct an object from them. The block may be called multiple times to
75
+ # construct multiple objects.
76
+ def decode_args_to_make(*args) #:nodoc:
77
+ shift_arg = lambda {|klass| args.shift if args.first.is_a?(klass) }
78
+ count = shift_arg[Fixnum]
79
+ name = shift_arg[Symbol] || :master
80
+ attributes = shift_arg[Hash] || {}
81
+ raise ArgumentError.new("Couldn't understand arguments") unless args.empty?
82
+
83
+ @blueprints ||= {}
84
+ blueprint = @blueprints[name]
85
+ raise NoBlueprintError.new(self, name) unless blueprint
86
+
87
+ if count.nil?
88
+ yield(blueprint, attributes)
89
+ else
90
+ Array.new(count) { yield(blueprint, attributes) }
91
+ end
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,3 @@
1
+ module Machinist
2
+ VERSION = "2.0"
3
+ end
data/lib/machinist.rb CHANGED
@@ -1,110 +1,5 @@
1
- require 'sham'
2
-
3
- module Machinist
1
+ require 'machinist/blueprint'
2
+ require 'machinist/exceptions'
3
+ require 'machinist/lathe'
4
+ require 'machinist/machinable'
4
5
 
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
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 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
104
-
105
- @@nerfed = false
106
- def self.nerfed?
107
- @@nerfed
108
- end
109
-
110
- end