fluent_fixtures 0.0.2

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