dataset 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/CHANGELOG +59 -0
  2. data/LICENSE +19 -0
  3. data/README +111 -0
  4. data/Rakefile +31 -0
  5. data/TODO +15 -0
  6. data/VERSION.yml +4 -0
  7. data/lib/dataset.rb +128 -0
  8. data/lib/dataset/base.rb +157 -0
  9. data/lib/dataset/collection.rb +19 -0
  10. data/lib/dataset/database/base.rb +30 -0
  11. data/lib/dataset/database/mysql.rb +34 -0
  12. data/lib/dataset/database/postgresql.rb +34 -0
  13. data/lib/dataset/database/sqlite3.rb +32 -0
  14. data/lib/dataset/extensions/cucumber.rb +20 -0
  15. data/lib/dataset/extensions/rspec.rb +21 -0
  16. data/lib/dataset/extensions/test_unit.rb +60 -0
  17. data/lib/dataset/instance_methods.rb +10 -0
  18. data/lib/dataset/load.rb +47 -0
  19. data/lib/dataset/record/fixture.rb +73 -0
  20. data/lib/dataset/record/meta.rb +66 -0
  21. data/lib/dataset/record/model.rb +50 -0
  22. data/lib/dataset/resolver.rb +110 -0
  23. data/lib/dataset/session.rb +51 -0
  24. data/lib/dataset/session_binding.rb +317 -0
  25. data/lib/dataset/version.rb +9 -0
  26. data/plugit/descriptor.rb +25 -0
  27. data/spec/dataset/cucumber_spec.rb +54 -0
  28. data/spec/dataset/database/base_spec.rb +21 -0
  29. data/spec/dataset/record/meta_spec.rb +14 -0
  30. data/spec/dataset/resolver_spec.rb +110 -0
  31. data/spec/dataset/rspec_spec.rb +133 -0
  32. data/spec/dataset/session_binding_spec.rb +198 -0
  33. data/spec/dataset/session_spec.rb +299 -0
  34. data/spec/dataset/test_unit_spec.rb +210 -0
  35. data/spec/fixtures/datasets/constant_not_defined.rb +0 -0
  36. data/spec/fixtures/datasets/ending_with_dataset.rb +2 -0
  37. data/spec/fixtures/datasets/exact_name.rb +2 -0
  38. data/spec/fixtures/datasets/not_a_dataset_base.rb +2 -0
  39. data/spec/fixtures/more_datasets/in_another_directory.rb +2 -0
  40. data/spec/models.rb +18 -0
  41. data/spec/schema.rb +26 -0
  42. data/spec/spec_helper.rb +47 -0
  43. data/spec/stubs/mini_rails.rb +18 -0
  44. data/spec/stubs/test_help.rb +1 -0
  45. data/tasks/dataset.rake +19 -0
  46. metadata +120 -0
@@ -0,0 +1,50 @@
1
+ module Dataset
2
+ module Record # :nodoc:
3
+
4
+ class Model # :nodoc:
5
+ attr_reader :attributes, :model, :meta, :symbolic_name, :session_binding
6
+
7
+ def initialize(meta, attributes, symbolic_name, session_binding)
8
+ @meta = meta
9
+ @attributes = attributes.stringify_keys
10
+ @symbolic_name = symbolic_name || object_id
11
+ @session_binding = session_binding
12
+ end
13
+
14
+ def record_class
15
+ meta.record_class
16
+ end
17
+
18
+ def id
19
+ model.id
20
+ end
21
+
22
+ def create
23
+ model = to_model
24
+ model.save!
25
+ model
26
+ end
27
+
28
+ def to_hash
29
+ to_model.attributes
30
+ end
31
+
32
+ def to_model
33
+ @model ||= begin
34
+ m = meta.record_class.new
35
+ attributes.each do |k,v|
36
+ if reflection = record_class.reflect_on_association(k.to_sym)
37
+ case v
38
+ when Symbol
39
+ v = session_binding.find_model(reflection.klass, v)
40
+ end
41
+ end
42
+ m.send "#{k}=", v
43
+ end
44
+ m
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,110 @@
1
+ module Dataset
2
+ # An error raised when a dataset class cannot be found.
3
+ #
4
+ class DatasetNotFound < StandardError
5
+ end
6
+
7
+ # A dataset may be referenced as a class or as a name. A Dataset::Resolver
8
+ # will take an identifier, whether a class or a name, and return the class.
9
+ #
10
+ class Resolver
11
+ cattr_accessor :default
12
+
13
+ def identifiers
14
+ @identifiers ||= {}
15
+ end
16
+
17
+ # Attempt to convert a name to a constant. With the identifier :people, it
18
+ # will search for 'PeopleDataset', then 'People'.
19
+ #
20
+ def resolve(identifier)
21
+ return identifier if identifier.is_a?(Class)
22
+ if constant = identifiers[identifier]
23
+ return constant
24
+ end
25
+
26
+ constant = resolve_class(identifier)
27
+ unless constant
28
+ constant = resolve_identifier(identifier)
29
+ end
30
+ identifiers[identifier] = constant
31
+ end
32
+
33
+ protected
34
+ def resolve_identifier(identifier) # :nodoc:
35
+ constant = resolve_class(identifier)
36
+ unless constant
37
+ raise Dataset::DatasetNotFound, "Could not find a dataset '#{identifier.to_s.camelize}' or '#{identifier.to_s.camelize + suffix}'."
38
+ end
39
+ constant
40
+ end
41
+
42
+ def resolve_class(identifier)
43
+ names = [identifier.to_s.camelize, identifier.to_s.camelize + suffix]
44
+ constant = resolve_these(names.reverse)
45
+ if constant && constant.superclass != ::Dataset::Base
46
+ raise Dataset::DatasetNotFound, "Found a class '#{constant.name}', but it does not subclass 'Dataset::Base'."
47
+ end
48
+ constant
49
+ end
50
+
51
+ def resolve_these(names) # :nodoc:
52
+ names.each do |name|
53
+ constant = name.constantize rescue nil
54
+ return constant if constant && constant.is_a?(Class)
55
+ end
56
+ nil
57
+ end
58
+
59
+ def suffix # :nodoc:
60
+ @suffix ||= 'Dataset'
61
+ end
62
+ end
63
+
64
+ # Resolves a dataset by looking for a file in the provided directory path
65
+ # that has a name matching the identifier. Of course, should the identifier
66
+ # be a class already, it is simply returned.
67
+ #
68
+ class DirectoryResolver < Resolver
69
+ def initialize(*paths)
70
+ @paths = paths
71
+ end
72
+
73
+ def <<(path)
74
+ @paths << path
75
+ end
76
+
77
+ protected
78
+ def resolve_identifier(identifier) # :nodoc:
79
+ @paths.each do |path|
80
+ file = File.join(path, identifier.to_s)
81
+ unless File.exists?(file + '.rb')
82
+ file = file + '_' + file_suffix
83
+ next unless File.exists?(file + '.rb')
84
+ end
85
+ require file
86
+ begin
87
+ return super
88
+ rescue Dataset::DatasetNotFound => dnf
89
+ if dnf.message =~ /\ACould not find/
90
+ raise Dataset::DatasetNotFound, "Found the dataset file '#{file + '.rb'}', but it did not define #{dnf.message.sub('Could not find ', '')}"
91
+ else
92
+ raise Dataset::DatasetNotFound, "Found the dataset file '#{file + '.rb'}' and a class #{dnf.message.sub('Found a class ', '')}"
93
+ end
94
+ end
95
+ end
96
+ raise DatasetNotFound, "Could not find a dataset file in #{@paths.inspect} having the name '#{identifier}.rb' or '#{identifier}_#{file_suffix}.rb'."
97
+ end
98
+
99
+ def file_suffix # :nodoc:
100
+ @file_suffix ||= suffix.downcase
101
+ end
102
+ end
103
+
104
+ # The default resolver, used by the Dataset::Sessions that aren't given a
105
+ # different instance. You can set this to something else in your
106
+ # test/spec_helper.
107
+ #
108
+ Resolver.default = Resolver.new
109
+
110
+ end
@@ -0,0 +1,51 @@
1
+ module Dataset
2
+ class Session # :nodoc:
3
+ attr_accessor :dataset_resolver
4
+
5
+ def initialize(database, dataset_resolver = Resolver.default)
6
+ @database = database
7
+ @dataset_resolver = dataset_resolver
8
+ @datasets = Hash.new
9
+ @load_stack = []
10
+ end
11
+
12
+ def add_dataset(test_class, dataset_identifier)
13
+ dataset = dataset_resolver.resolve(dataset_identifier)
14
+ if dataset.used_datasets
15
+ dataset.used_datasets.each { |used_dataset| self.add_dataset(test_class, used_dataset) }
16
+ end
17
+ datasets_for(test_class) << dataset
18
+ end
19
+
20
+ def datasets_for(test_class)
21
+ if test_class.superclass
22
+ @datasets[test_class] ||= Collection.new(datasets_for(test_class.superclass) || [])
23
+ end
24
+ end
25
+
26
+ def load_datasets_for(test_class)
27
+ datasets = datasets_for(test_class)
28
+ if last_load = @load_stack.last
29
+ if last_load.datasets == datasets
30
+ current_load = Reload.new(last_load)
31
+ elsif last_load.datasets.subset?(datasets)
32
+ @database.capture(last_load.datasets)
33
+ current_load = Load.new(datasets, last_load.dataset_binding)
34
+ current_load.execute(last_load.datasets, @dataset_resolver)
35
+ @load_stack.push(current_load)
36
+ else
37
+ @load_stack.pop
38
+ last_load = @load_stack.last
39
+ @database.restore(last_load.datasets) if last_load
40
+ current_load = load_datasets_for(test_class)
41
+ end
42
+ else
43
+ @database.clear
44
+ current_load = Load.new(datasets, @database)
45
+ current_load.execute([], @dataset_resolver)
46
+ @load_stack.push(current_load)
47
+ end
48
+ current_load
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,317 @@
1
+ module Dataset
2
+ # An error that will be raised when an attempt is made to load a named model
3
+ # that doesn't exist. For example, if you do people(:jenny), and yet no
4
+ # record was ever created with the symbolic name :jenny, this error will be
5
+ # raised.
6
+ #
7
+ class RecordNotFound < StandardError
8
+ def initialize(record_type, symbolic_name)
9
+ super "There is no '#{record_type.name}' found for the symbolic name ':#{symbolic_name}'."
10
+ end
11
+ end
12
+
13
+ # Whenever you use Dataset::RecordMethods, you will get finder methods in
14
+ # your tests that help you load instances of the records you have created
15
+ # (or named models).
16
+ #
17
+ # create_record :person, :jimmy, :name => 'Jimmy'
18
+ # person_id(:jimmy) => The id was captured from create_record
19
+ # people(:jimmy) => The same as Jimmy.find(person_id(:jimmy))
20
+ #
21
+ # The methods will not exist in a test unless it utilizes a dataset (or
22
+ # defines one itself through the block technique) that creates a record for
23
+ # the type.
24
+ #
25
+ # You may also pass multiple names to these methods, which will have them
26
+ # return an array of values.
27
+ #
28
+ # people(:jimmy, :jane, :jeff) => [<# Person :name => 'Jimmy'>, <# Person :name => 'Jane'>, <# Person :name => 'Jeff'>]
29
+ # person_id(:jimmy, :jane, :jeff) => [1, 2, 3]
30
+ #
31
+ # NOTE the plurality of the instance finder, versus the singularity of the
32
+ # id finder.
33
+ #
34
+ # == Single Table Inheritence
35
+ #
36
+ # class Person < ActiveRecord::Base; end
37
+ # class User < Person; end
38
+ #
39
+ # create_record :user, :bobby, :name => 'Bobby'
40
+ #
41
+ # people(:bobby) OR users(:bobby)
42
+ #
43
+ module ModelFinders
44
+ def create_finder(record_meta) # :nodoc:
45
+ @finders_generated ||= []
46
+
47
+ return if @finders_generated.include?(record_meta)
48
+
49
+ record_meta.model_finder_names.each do |finder_name|
50
+ unless instance_methods.include?(finder_name)
51
+ define_method finder_name do |*symbolic_names|
52
+ names = Array(symbolic_names)
53
+ models = names.inject([]) do |c,n|
54
+ c << dataset_session_binding.find_model(record_meta, n); c
55
+ end
56
+ names.size == 1 ? models.first : models
57
+ end
58
+ end
59
+ end
60
+
61
+ record_meta.id_finder_names.each do |finder_name|
62
+ unless instance_methods.include?(finder_name)
63
+ define_method finder_name do |*symbolic_names|
64
+ names = Array(symbolic_names)
65
+ ids = names.inject([]) do |c,n|
66
+ c << dataset_session_binding.find_id(record_meta, n); c
67
+ end
68
+ names.size == 1 ? ids.first : ids
69
+ end
70
+ end
71
+ end
72
+
73
+ @finders_generated << record_meta
74
+ end
75
+ end
76
+
77
+ # Any Dataset::Base subclass, dataset block, or test method in a
78
+ # dataset-using test context (including setup/teardown/before/after) may
79
+ # create and access models through these methods. Note that you should use
80
+ # Dataset::ModelFinders if you can for finding your created data.
81
+ #
82
+ module RecordMethods
83
+
84
+ # Similar to old fashioned fixtures, this will do a direct database
85
+ # insert, without running any validations or preventing you from writing
86
+ # attr_protected attributes. Very nice for speed, but kind of a pain if
87
+ # you have complex structures or hard to keep right validations.
88
+ #
89
+ # create_record :type, :symbolic_name, :attr1 => 'value', :attr2 => 'value', :etc => 'etc'
90
+ #
91
+ # The _symbolic_name_ is an optional parameter. You may replace _type_
92
+ # with an ActiveRecord::Base subclass or anything that works with:
93
+ #
94
+ # to_s.classify.constantize
95
+ #
96
+ # The id of the model will be a hash of the symbolic name.
97
+ #
98
+ def create_record(*args)
99
+ dataset_session_binding.create_record(*args)
100
+ end
101
+
102
+ # This will instantiate your model class and assign each attribute WITHOUT
103
+ # using mass assignment. Validations will be run. Very nice for complex
104
+ # structures or hard to keep right validations, but potentially a bit
105
+ # slower, since it runs through all that ActiveRecord code.
106
+ #
107
+ # create_model :type, :symbolic_name, :attr1 => 'value', :attr2 => 'value', :etc => 'etc'
108
+ #
109
+ # The _symbolic_name_ is an optional parameter. You may replace _type_
110
+ # with an ActiveRecord::Base subclass or anything that works with:
111
+ #
112
+ # to_s.classify.constantize
113
+ #
114
+ # The id of the record will be kept from the instance that is saved.
115
+ #
116
+ def create_model(*args)
117
+ dataset_session_binding.create_model(*args)
118
+ end
119
+
120
+ # Dataset will track each of the records it creates by symbolic name to
121
+ # id. When you need the id of a record, there is no need to go to the
122
+ # database.
123
+ #
124
+ # find_id :person, :bobby => 23425234
125
+ #
126
+ # You may pass one name or many, with many returning an Array of ids.
127
+ #
128
+ def find_id(*args)
129
+ dataset_session_binding.find_id(*args)
130
+ end
131
+
132
+ # Dataset will track each of the records it creates by symbolic name to
133
+ # id. When you need an instance of a record, the stored id will be used to
134
+ # do the fastest lookup possible: Person.find(23425234).
135
+ #
136
+ # find_model :person, :bobby => <#Person :id => 23425234, :name => 'Bobby'>
137
+ #
138
+ # You may pass one name or many, with many returning an Array of
139
+ # instances.
140
+ #
141
+ def find_model(*args)
142
+ dataset_session_binding.find_model(*args)
143
+ end
144
+
145
+ # This is a great help when you want to create records in a custom helper
146
+ # method, then make it and maybe things associated to it available to
147
+ # tests through the Dataset::ModelFinders.
148
+ #
149
+ # thingy = create_very_complex_thingy_and_stuff
150
+ # name_model thingy, :thingy_bob
151
+ # name_model thingy.part, :thingy_part
152
+ #
153
+ # In tests:
154
+ #
155
+ # thingies(:thingy_bob)
156
+ # parts(:thingy_part)
157
+ #
158
+ def name_model(*args)
159
+ dataset_session_binding.name_model(*args)
160
+ end
161
+
162
+ # Converts string names into symbols for use in naming models
163
+ #
164
+ # name_to_sym 'my name' => :my_name
165
+ # name_to_sym 'RPaul' => :r_paul
166
+ #
167
+ def name_to_sym(name)
168
+ dataset_session_binding.name_to_sym(name)
169
+ end
170
+ end
171
+
172
+ class SessionBinding # :nodoc:
173
+ attr_reader :database, :parent_binding
174
+ attr_reader :model_finders, :record_methods
175
+ attr_reader :block_variables
176
+
177
+ def initialize(database_or_parent_binding)
178
+ @id_cache = Hash.new {|h,k| h[k] = {}}
179
+ @record_methods = new_record_methods_module
180
+ @model_finders = new_model_finders_module
181
+ @block_variables = Hash.new
182
+
183
+ case database_or_parent_binding
184
+ when Dataset::SessionBinding
185
+ @parent_binding = database_or_parent_binding
186
+ @database = parent_binding.database
187
+ @model_finders.module_eval { include database_or_parent_binding.model_finders }
188
+ @block_variables.update(database_or_parent_binding.block_variables)
189
+ else
190
+ @database = database_or_parent_binding
191
+ end
192
+ end
193
+
194
+ def copy_block_variables(dataset_block)
195
+ dataset_block.instance_variables.each do |name|
196
+ self.block_variables[name] = dataset_block.instance_variable_get(name)
197
+ end
198
+ end
199
+
200
+ def create_model(record_type, *args)
201
+ insert(Dataset::Record::Model, record_type, *args)
202
+ end
203
+
204
+ def create_record(record_type, *args)
205
+ insert(Dataset::Record::Fixture, record_type, *args)
206
+ end
207
+
208
+ def find_id(record_type_or_meta, symbolic_name)
209
+ record_meta = record_meta_for_type(record_type_or_meta)
210
+ if local_id = @id_cache[record_meta.id_cache_key][symbolic_name]
211
+ local_id
212
+ elsif !parent_binding.nil?
213
+ parent_binding.find_id record_meta, symbolic_name
214
+ else
215
+ raise RecordNotFound.new(record_meta, symbolic_name)
216
+ end
217
+ end
218
+
219
+ def find_model(record_type_or_meta, symbolic_name)
220
+ record_meta = record_meta_for_type(record_type_or_meta)
221
+ if local_id = @id_cache[record_meta.id_cache_key][symbolic_name]
222
+ record_meta.record_class.find local_id
223
+ elsif !parent_binding.nil?
224
+ parent_binding.find_model record_meta, symbolic_name
225
+ else
226
+ raise RecordNotFound.new(record_meta, symbolic_name)
227
+ end
228
+ end
229
+
230
+ def install_block_variables(target)
231
+ block_variables.each do |k,v|
232
+ target.instance_variable_set(k,v)
233
+ end
234
+ end
235
+
236
+ def name_model(record, symbolic_name)
237
+ record_meta = database.record_meta(record.class)
238
+ @model_finders.create_finder(record_meta)
239
+ @id_cache[record_meta.id_cache_key][symbolic_name] = record.id
240
+ record
241
+ end
242
+
243
+ def record_meta_for_type(record_type)
244
+ record_type.is_a?(Record::Meta) ? record_type : begin
245
+ record_class = resolve_record_class(record_type)
246
+ database.record_meta(record_class)
247
+ end
248
+ end
249
+
250
+ def name_to_sym(name)
251
+ name.to_s.underscore.gsub("'", "").gsub("\"", "").gsub(" ", "_").to_sym if name
252
+ end
253
+
254
+ protected
255
+ def insert(dataset_record_class, record_type, *args)
256
+ symbolic_name, attributes = extract_creation_arguments args
257
+ record_meta = record_meta_for_type(record_type)
258
+ record = dataset_record_class.new(record_meta, attributes, symbolic_name, self)
259
+ return_value = nil
260
+
261
+ @model_finders.create_finder(record_meta)
262
+ ActiveRecord::Base.silence do
263
+ return_value = record.create
264
+ @id_cache[record_meta.id_cache_key][symbolic_name] = record.id
265
+ end
266
+ return_value
267
+ end
268
+
269
+ def extract_creation_arguments(arguments)
270
+ if arguments.size == 2 && arguments.last.kind_of?(Hash)
271
+ arguments
272
+ elsif arguments.size == 1 && arguments.last.kind_of?(Hash)
273
+ [nil, arguments.last]
274
+ elsif arguments.size == 1 && arguments.last.kind_of?(Symbol)
275
+ [arguments.last, Hash.new]
276
+ else
277
+ [nil, Hash.new]
278
+ end
279
+ end
280
+
281
+ def new_model_finders_module
282
+ mod = Module.new
283
+ dataset_binding = self
284
+ mod.module_eval do
285
+ define_method :dataset_session_binding do
286
+ dataset_binding
287
+ end
288
+ end
289
+ mod.extend ModelFinders
290
+ mod
291
+ end
292
+
293
+ def new_record_methods_module
294
+ mod = Module.new do
295
+ include RecordMethods
296
+ end
297
+ dataset_binding = self
298
+ mod.module_eval do
299
+ define_method :dataset_session_binding do
300
+ dataset_binding
301
+ end
302
+ end
303
+ mod
304
+ end
305
+
306
+ def resolve_record_class(record_type)
307
+ case record_type
308
+ when Symbol
309
+ resolve_record_class record_type.to_s.singularize.camelize
310
+ when Class
311
+ record_type
312
+ when String
313
+ record_type.constantize
314
+ end
315
+ end
316
+ end
317
+ end