fluent_fixtures 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,106 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'fluent_fixtures' unless defined?( FluentFixtures )
5
+
6
+
7
+ module FluentFixtures::DSL
8
+
9
+ #
10
+ # Accessors for extended fixture modules
11
+ #
12
+
13
+ ##
14
+ # The Hash of decorators declared for this fixture module
15
+ attr_reader :decorators
16
+
17
+ ##
18
+ # The name of the base fixture for the fixture module as a Symbol
19
+ attr_accessor :base_fixture
20
+
21
+
22
+ ##
23
+ # The FluentFixtures::Collection the fixture is part of
24
+ attr_accessor :collection
25
+
26
+
27
+ #
28
+ # Fixture API
29
+ #
30
+
31
+ ### Declare a base fixture for the current module called +name+, with an optional
32
+ ### initial decorator as a +block+. If no +name+ is given, one is chosen based on the
33
+ ### name of the declaring module.
34
+ def base( name=nil, &block )
35
+ name ||= self.collection.default_base_fixture_name( self )
36
+
37
+ self.base_fixture = name
38
+ self.decorators[ :_ ] = block if block
39
+
40
+ self.collection.add_base_fixture( name, self )
41
+ end
42
+
43
+
44
+ ### Get/set the Class the fixture will use.
45
+ def fixtured_class( new_class=nil )
46
+ @fixtured_class = new_class if new_class
47
+ return @fixtured_class
48
+ end
49
+
50
+
51
+ ### Declare a decorator for the fixture with the specified +name+ that will use the
52
+ ### given +block+.
53
+ def decorator( name, &block )
54
+ self.decorators[ name.to_sym ] = block
55
+ end
56
+
57
+
58
+ ### Returns +true+ if there is a decorator with the specified +name+.
59
+ def decorator?( name )
60
+ return self.decorators.key?( name.to_sym )
61
+ end
62
+ alias_method :has_decorator?, :decorator?
63
+
64
+
65
+ ### Declare a +new_name+ for the decorator declarted with with +original_name+.
66
+ def alias_decorator( new_name, original_name )
67
+ block = self.decorators[ original_name.to_sym ] or
68
+ raise ScriptError, "undefined decorator %p" % [ original_name ]
69
+ self.decorators[ new_name.to_sym ] = block
70
+ end
71
+
72
+
73
+ ### Add a callback to the fixture that will passed new instances after all
74
+ ### decorators have been applied and immediately before it's saved. The results of
75
+ ### the block will be used as the fixtured instance. This can be
76
+ ### used for tables with fixed rows to use `find_or_create` or similar.
77
+ def before_saving( &block )
78
+ define_singleton_method( :call_before_saving, &block )
79
+ end
80
+
81
+
82
+ ### Return an instance of Cozy::FluentFixtures::FluentFactory for the base fixture
83
+ ### of the receiving module.
84
+ def factory( *args, &block )
85
+ return FluentFixtures::Factory.new( self, *args, &block )
86
+ end
87
+
88
+
89
+ ### Return an unsaved instance of the fixtured class with the specified +args+
90
+ ### and +block+, applying the base decorator if there is one.
91
+ def fixtured_instance( *args, &block )
92
+ fixclass = self.fixtured_class or
93
+ raise ScriptError, "%p doesn't declare its fixtured class!" % [ self ]
94
+
95
+ instance = fixclass.new( *args, &block )
96
+
97
+ if (( base_decorator = self.decorators[:_] ))
98
+ self.log.debug "Applying base decorator to %p" % [ instance ]
99
+ instance.instance_exec( &base_decorator )
100
+ end
101
+
102
+ return instance
103
+ end
104
+
105
+
106
+ end # module FluentFixtures::DSL
@@ -0,0 +1,201 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'loggability'
5
+ require 'fluent_fixtures' unless defined?( FluentFixtures )
6
+
7
+
8
+ # The fluent fixture monadic factory class.
9
+ class FluentFixtures::Factory
10
+ extend Loggability
11
+
12
+
13
+ # The methods to look for to save new instances when #create is called
14
+ CREATE_METHODS = %i[ save_changes save ]
15
+
16
+
17
+ # Loggability API -- log to the FluentFixtures logger
18
+ log_to :fluent_fixtures
19
+
20
+
21
+ ### Create a new FluentFactory that will act as a monadic factory for the specified
22
+ ### +fixture_module+, and use the given +args+ in the construction of new objects.
23
+ def initialize( fixture_module, *args, &block )
24
+ @fixture_module = fixture_module
25
+ @constructor_args = args
26
+ @constructor_block = block
27
+ @decorators = []
28
+ end
29
+
30
+
31
+ ### Copy constructor -- make a distinct copy of the clone's decorators.
32
+ def initialize_copy( original )
33
+ @decorators = @decorators.dup
34
+ end
35
+
36
+
37
+ ##
38
+ # The fixture module that contains the decorator declarations
39
+ attr_reader :fixture_module
40
+
41
+ ##
42
+ # The decorators that will be applied to the fixtured object when it's created.
43
+ attr_reader :decorators
44
+
45
+ ##
46
+ # The Array of arguments to pass to the constructor when creating a new fixtured object.
47
+ attr_reader :constructor_args
48
+
49
+ ##
50
+ # The block to pass to the constructor when creating a new fixtured object.
51
+ attr_reader :constructor_block
52
+
53
+
54
+ ### Return a new clone of the receiver with an additional decorator composed of the
55
+ ### specified +name+, +args+, and +block+.
56
+ def mutate( name, *args, &block )
57
+ new_instance = self.dup
58
+ new_instance.decorators << [ name, args, block ]
59
+ return new_instance
60
+ end
61
+
62
+
63
+ ### Create an instance, apply declared decorators in order, and return the resulting
64
+ ### object.
65
+ def instance( *args, &block )
66
+ instance = self.fixture_module.
67
+ fixtured_instance( *self.constructor_args, &self.constructor_block )
68
+
69
+ self.decorators.each do |decorator_name, args, block|
70
+ # :TODO: Reify other fixtures in `args` here?
71
+ if !decorator_name
72
+ self.apply_inline_decorator( instance, block )
73
+ elsif self.fixture_module.decorators.key?( decorator_name )
74
+ self.apply_named_decorator( instance, args, decorator_name )
75
+ else
76
+ self.apply_method_decorator( instance, args, decorator_name, block )
77
+ end
78
+ end
79
+
80
+ # If the factory was called with a block, use it as a final decorator before
81
+ # returning it.
82
+ if block
83
+ self.log.debug "Applying inline decorator %p" % [ block ]
84
+ if block.arity.zero?
85
+ instance.instance_exec( *args, &block )
86
+ else
87
+ block.call( instance, *args )
88
+ end
89
+ end
90
+
91
+ return instance
92
+ end
93
+
94
+
95
+ ### Return a saved #instance of the fixtured object.
96
+ def create( *args, &block )
97
+ obj = self.with_transaction do
98
+ obj = self.instance( *args, &block )
99
+ obj = self.fixture_module.call_before_saving( obj ) if
100
+ self.fixture_module.respond_to?( :call_before_saving )
101
+
102
+ self.try_to_save( obj )
103
+
104
+ obj
105
+ end
106
+
107
+ return obj
108
+ end
109
+
110
+
111
+ ### Return a copy of the factory that will apply the specified +block+ as a decorator.
112
+ def decorated_with( &block )
113
+ return self.mutate( nil, &block )
114
+ end
115
+
116
+
117
+ ### Return a human-readable representation of the object suitable for debugging.
118
+ def inspect
119
+ decorator_description = self.decorators.map( &:first ).join( ' + ' )
120
+
121
+ return "#<%p:%0#16x for %p%s>" % [
122
+ self.class,
123
+ self.__id__ * 2,
124
+ self.fixture_module,
125
+ decorator_description.empty? ? '' : ' + ' + decorator_description
126
+ ]
127
+ end
128
+
129
+
130
+ #########
131
+ protected
132
+ #########
133
+
134
+ ### Apply a decorator +block+ added to the factory via #decorated_with to the +instance+.
135
+ def apply_inline_decorator( instance, block )
136
+ self.log.debug "Applying anonymous inline decorator %p" % [ block ]
137
+ if block.arity.nonzero?
138
+ block.call( instance )
139
+ else
140
+ instance.instance_eval( &block )
141
+ end
142
+ end
143
+
144
+
145
+ ### Apply a decorator declared in the fixture module by the given
146
+ ### +decorator_name+ to the specified +instance+.
147
+ def apply_named_decorator( instance, args, decorator_name )
148
+ decorator_block = self.fixture_module.decorators[ decorator_name ]
149
+ self.log.debug "Applying decorator %p (%p) to a %p with args: %p" %
150
+ [ decorator_name, decorator_block, instance.class, args ]
151
+ instance.instance_exec( *args, &decorator_block )
152
+ end
153
+
154
+
155
+ ### Apply a decorator declared by calling the proxy method with the specified
156
+ ### +name+, +args+, and +block+ to the given +instance+.
157
+ def apply_method_decorator( instance, args, name, block )
158
+ self.log.debug "Mutating instance %p with regular method %p( %p, %p )" %
159
+ [ instance, name, args, block ]
160
+ if block
161
+ instance.public_send( name, *args )
162
+ else
163
+ instance.public_send( name, *args, &block )
164
+ end
165
+ end
166
+
167
+
168
+ ### Look for common transaction mechanisms on the fixtured class, and wrap one of
169
+ ### them around the +block+ if one exists. If no transaction mechanism can be found,
170
+ ### just yield to the block.
171
+ def with_transaction( &block )
172
+ fixtured_class = self.fixture_module.fixtured_class
173
+ if fixtured_class.respond_to?( :db )
174
+ self.log.debug "Using db.transaction for creation."
175
+ return fixtured_class.db.transaction( &block )
176
+ else
177
+ yield
178
+ end
179
+ end
180
+
181
+
182
+ ### Try various methods for saving the given +object+, logging a warning if it doesn't
183
+ ### respond to any of them.
184
+ def try_to_save( object )
185
+ CREATE_METHODS.each do |methodname|
186
+ return object.public_send( methodname ) if object.respond_to?( methodname )
187
+ end
188
+
189
+ self.log.warn "create: don't know how to save %p" % [ object ]
190
+ end
191
+
192
+
193
+ ### Proxy method -- look up the decorator with the same name as the method being called,
194
+ ### and if one is found, returned a new instance of the factory with the additional
195
+ ### decorator.
196
+ def method_missing( sym, *args, &block )
197
+ return self.mutate( sym, *args, &block )
198
+ end
199
+
200
+ end # class FluentFixtures::Factory
201
+
@@ -0,0 +1,29 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'loggability'
6
+
7
+
8
+ # Fluent fixtures
9
+ module FluentFixtures
10
+ extend Loggability
11
+
12
+
13
+ # Package version
14
+ VERSION = '0.0.2'
15
+
16
+ # Version control revision
17
+ REVISION = %q$Revision: e500071dd9c0 $
18
+
19
+
20
+ # Loggability API -- set up a named logger
21
+ log_as :fluent_fixtures
22
+
23
+
24
+ require 'fluent_fixtures/collection'
25
+ require 'fluent_fixtures/dsl'
26
+ require 'fluent_fixtures/factory'
27
+
28
+ end # module FluentFixtures
29
+
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'fluent_fixtures/collection'
6
+
7
+
8
+ describe FluentFixtures::Collection do
9
+
10
+ let( :collection ) do
11
+ mod = Module.new
12
+ mod.extend( described_class )
13
+ mod
14
+ end
15
+
16
+
17
+ it "registers its fixture modules when they are extended if they have a name" do
18
+ mod = Module.new do
19
+ def self::name; "NamedFixture"; end
20
+ end
21
+ mod.extend( collection )
22
+
23
+ expect( collection.modules ).to include( namedfixture: mod )
24
+ end
25
+
26
+
27
+ it "registers its fixture modules when they declare a base if they are anonymous" do
28
+ mod = Module.new
29
+ mod.extend( collection )
30
+
31
+ expect( collection.modules ).to be_empty
32
+
33
+ mod.base( :namedfixture )
34
+
35
+ expect( collection.modules ).to include( namedfixture: mod )
36
+ end
37
+
38
+
39
+ it "generates a factory constructor for the fixture when it regsters a base" do
40
+ mod = Module.new
41
+ mod.extend( collection )
42
+ mod.base( :new_fixture )
43
+
44
+ expect( collection ).to respond_to( :new_fixture )
45
+ expect( collection.new_fixture ).to be_a( FluentFixtures::Factory )
46
+ expect( collection.new_fixture.fixture_module ).to be( mod )
47
+ end
48
+
49
+
50
+ it "raises if two base fixtures are registered with the same name" do
51
+ mod1 = Module.new
52
+ mod1.extend( collection )
53
+
54
+ mod2 = Module.new
55
+ mod2.extend( collection )
56
+
57
+ mod1.base( :base_fixture )
58
+
59
+ expect {
60
+ mod2.base( :base_fixture )
61
+ }.to raise_error( ScriptError, /already have a base fixture.*base_fixture/i )
62
+ end
63
+
64
+
65
+ it "can be reset, removing all of its registered fixture modules and their factory methods" do
66
+ mod1 = Module.new
67
+ mod1.extend( collection )
68
+
69
+ mod2 = Module.new
70
+ mod2.extend( collection )
71
+
72
+ mod1.base( :fixture1 )
73
+ mod2.base( :fixture2 )
74
+
75
+ expect( collection.modules ).to include( fixture1: mod1, fixture2: mod2 )
76
+ expect( collection ).to respond_to( :fixture1 )
77
+ expect( collection ).to respond_to( :fixture2 )
78
+
79
+ collection.reset!
80
+
81
+ expect( collection.modules ).to be_empty
82
+ expect( collection ).to_not respond_to( :fixture1 )
83
+ expect( collection ).to_not respond_to( :fixture2 )
84
+ end
85
+
86
+
87
+ it "can load fixtures by name" do
88
+ # Mocking Kernel.require via the class under test
89
+ expect( collection ).to receive( :require ).
90
+ with( "fixtures/foo" ).and_return( false )
91
+ expect( collection ).to receive( :require ).
92
+ with( "fixtures/bar" ).and_return( false )
93
+
94
+ collection.load( :foo, :bar )
95
+ end
96
+
97
+
98
+ it "can load all fixtures" do
99
+ expect( Gem ).to receive( :find_files ).with( "fixtures/*.rb" ).and_return([
100
+ "fixtures/foo.rb",
101
+ "fixtures/bar.rb",
102
+ "fixtures/baz.rb"
103
+ ])
104
+
105
+ # Mocking Kernel.require via the class under test
106
+ expect( collection ).to receive( :require ).
107
+ with( "fixtures/foo" ).and_return( false )
108
+ expect( collection ).to receive( :require ).
109
+ with( "fixtures/bar" ).and_return( false )
110
+ expect( collection ).to receive( :require ).
111
+ with( "fixtures/baz" ).and_return( false )
112
+
113
+ collection.load_all
114
+ end
115
+
116
+
117
+ it "allows the loading prefix to be customized" do
118
+ collection.fixture_path_prefix( 'acme/fixtures' )
119
+ expect( collection ).to receive( :require ).
120
+ with( "acme/fixtures/foo" ).and_return( false )
121
+
122
+ collection.load( :foo )
123
+ end
124
+
125
+
126
+ describe "fixture extension" do
127
+
128
+ let( :extending_module ) do
129
+ mod = Module.new do
130
+ def self::name ; "CollectionTests"; end
131
+ end
132
+ mod.extend( collection )
133
+ mod
134
+ end
135
+
136
+
137
+ it "adds instance data to its fixture modules" do
138
+ expect( extending_module.decorators ).to be_a( Hash )
139
+ end
140
+
141
+ end
142
+
143
+
144
+ end
145
+