machinist 1.0.6 → 2.0

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