planter 0.4.2 → 1.0.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.
- checksums.yaml +4 -4
- data/README.md +129 -40
- data/lib/generators/planter/adapter_generator.rb +19 -14
- data/lib/generators/planter/initializer_generator.rb +7 -1
- data/lib/generators/planter/seeder_generator.rb +109 -9
- data/lib/planter/adapters/active_record.rb +106 -31
- data/lib/planter/config.rb +2 -2
- data/lib/planter/csv_data_source.rb +54 -0
- data/lib/planter/record_attributes.rb +113 -0
- data/lib/planter/seed_context.rb +83 -0
- data/lib/planter/seeder.rb +70 -121
- data/lib/planter/validator.rb +296 -0
- data/lib/planter/version.rb +3 -3
- data/lib/planter.rb +16 -2
- data/lib/tasks/planter_tasks.rake +31 -5
- metadata +5 -1
data/lib/planter/seeder.rb
CHANGED
|
@@ -4,30 +4,35 @@ module Planter
|
|
|
4
4
|
##
|
|
5
5
|
# Class that seeders should inherit from. Seeders should be in +db/seeds+,
|
|
6
6
|
# and named +TABLE_seeder.rb+, where +TABLE+ is the name of the table being
|
|
7
|
-
# seeded (
|
|
8
|
-
# the table, you'll need to specify the table with the +
|
|
7
|
+
# seeded (for example, +users_seeder.rb+). If your seeder is named differently
|
|
8
|
+
# than the table, you'll need to specify the table with the +table+ option. The
|
|
9
9
|
# seeder's class name should be the same as the file name, but camelized. So,
|
|
10
10
|
# +UsersSeeder+. The directory where the seeder files are located can be
|
|
11
11
|
# changed via an initializer.
|
|
12
12
|
#
|
|
13
|
+
# To generate a seeder with a specific style, pass +--seeding-method=csv+,
|
|
14
|
+
# +--seeding-method=data-array+, or +--seeding-method=custom+ to
|
|
15
|
+
# +rails generate planter:seeder+. When the +csv+ method is used, the generator
|
|
16
|
+
# also creates a CSV file with headers pulled from the table.
|
|
17
|
+
#
|
|
13
18
|
# The most basic way to seed is to have a CSV file with the same name as the
|
|
14
19
|
# table in +db/seed_files/+. So, +users.csv+. This CSV should have the
|
|
15
|
-
# table's column names as
|
|
16
|
-
# should look like the following. Note that +:csv_name+ and +:
|
|
17
|
-
# required if your seeder or
|
|
20
|
+
# table's column names as headers. To seed using this method, your class
|
|
21
|
+
# should look like the following. Note that +:csv_name+ and +:table+ are only
|
|
22
|
+
# required if your seeder or CSV are named differently than the table being
|
|
18
23
|
# seeded. The directory where the seed files are kept can be changed via an
|
|
19
24
|
# initializer.
|
|
20
25
|
# # db/seeds/users_seeder.rb
|
|
21
26
|
# require 'planter'
|
|
22
27
|
# class UsersSeeder < Planter::Seeder
|
|
23
|
-
# seeding_method :csv, csv_name: :users,
|
|
28
|
+
# seeding_method :csv, csv_name: :users, table: :users
|
|
24
29
|
# end
|
|
25
30
|
#
|
|
26
31
|
# Another way to seed is to create records from a data array. To do this,
|
|
27
32
|
# your class must implement a +data+ attribute or method, which is an array
|
|
28
33
|
# of hashes. Note that this class already provides the +attr_reader+ for this
|
|
29
34
|
# attribute, so the most you have to do is create instance variables in your
|
|
30
|
-
# constructor. If
|
|
35
|
+
# constructor. If you want your data to be different for each new record
|
|
31
36
|
# (via Faker, +Array#sample+, etc.), you'll probably want to supply a method
|
|
32
37
|
# called data that returns an array of new data each time.
|
|
33
38
|
# require 'planter'
|
|
@@ -39,13 +44,12 @@ module Planter
|
|
|
39
44
|
# end
|
|
40
45
|
#
|
|
41
46
|
# In both of the above methods, you can specify a +parent+ association, which
|
|
42
|
-
# is
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# using +Faker+ or a similar library.
|
|
47
|
+
# is interpreted by the configured adapter. With the default Active Record
|
|
48
|
+
# adapter, +parent+ is the +belongs_to+ association name on a model-backed
|
|
49
|
+
# table. When specified, records will be created for each record in the parent
|
|
50
|
+
# table. Note that nothing is automatically done to prevent any validation
|
|
51
|
+
# errors; you must do this on your own, most likely using +Faker+ or a similar
|
|
52
|
+
# library.
|
|
49
53
|
# require 'planter'
|
|
50
54
|
# class UsersSeeder < Planter::Seeder
|
|
51
55
|
# seeding_method :data_array, parent: :person
|
|
@@ -55,7 +59,7 @@ module Planter
|
|
|
55
59
|
# end
|
|
56
60
|
#
|
|
57
61
|
# You can also set +number_of_records+ to determine how many times each
|
|
58
|
-
# record in the +data+ array will
|
|
62
|
+
# record in the +data+ array will be created. The default is 1. Note that if
|
|
59
63
|
# this attribute is set alongside +parent+, +number_of_records+ will be how
|
|
60
64
|
# many records will be created for each record in the parent table.
|
|
61
65
|
# require 'planter'
|
|
@@ -68,7 +72,7 @@ module Planter
|
|
|
68
72
|
#
|
|
69
73
|
# By default, all fields are used to look up the record. If it already
|
|
70
74
|
# exists, it is not re-created. If you have specific fields that a record
|
|
71
|
-
# should be looked
|
|
75
|
+
# should be looked up by, you can pass the +unique_columns+ option. This will
|
|
72
76
|
# attempt to look up the record by those fields only, and if one doesn't
|
|
73
77
|
# exist, one will be created with the rest of the attributes. An example of
|
|
74
78
|
# when this would be useful is with Devise; you can't pass +password+ in the
|
|
@@ -97,14 +101,14 @@ module Planter
|
|
|
97
101
|
# attribute when using +data_array+ seeding method, although it's probably
|
|
98
102
|
# more likely that you'll want to define a method that returns a new set of
|
|
99
103
|
# data each time (via +Faker+, +Array#sample+, etc.). When using +csv+,
|
|
100
|
-
# +data+ will be set to the data within the
|
|
104
|
+
# +data+ will be set to the data within the CSV. You can override this.
|
|
101
105
|
#
|
|
102
106
|
# @return [Array]
|
|
103
107
|
attr_reader :data
|
|
104
108
|
|
|
105
109
|
##
|
|
106
110
|
# A hash of user-defined column names and procs to be run on values. This
|
|
107
|
-
# is most useful for when seeding from
|
|
111
|
+
# is most useful for when seeding from CSV, and you need to transform, say,
|
|
108
112
|
# 'true' (String) into true (Boolean). The user may define this as an
|
|
109
113
|
# instance variable, or define a method that returns the hash.
|
|
110
114
|
#
|
|
@@ -145,16 +149,15 @@ module Planter
|
|
|
145
149
|
class_attribute :unique_columns
|
|
146
150
|
|
|
147
151
|
##
|
|
148
|
-
# The
|
|
149
|
-
#
|
|
152
|
+
# The table being seeded. If the table name you need is different, change
|
|
153
|
+
# via +seeding_method+.
|
|
150
154
|
#
|
|
151
155
|
# @return [String]
|
|
152
|
-
class_attribute :
|
|
156
|
+
class_attribute :table_name
|
|
153
157
|
|
|
154
158
|
##
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
# class must set this attribute via +seeding_method+.
|
|
159
|
+
# Adapter-defined parent relation. Records in the +data+ array will be
|
|
160
|
+
# created for each record in the parent relation.
|
|
158
161
|
#
|
|
159
162
|
# @return [String]
|
|
160
163
|
class_attribute :parent
|
|
@@ -168,7 +171,7 @@ module Planter
|
|
|
168
171
|
class_attribute :number_of_records
|
|
169
172
|
|
|
170
173
|
##
|
|
171
|
-
# The
|
|
174
|
+
# The CSV file corresponding to the table.
|
|
172
175
|
#
|
|
173
176
|
# @return [String]
|
|
174
177
|
class_attribute :csv_name
|
|
@@ -188,7 +191,7 @@ module Planter
|
|
|
188
191
|
#
|
|
189
192
|
# @kwarg [Integer] number_of_records
|
|
190
193
|
#
|
|
191
|
-
# @kwarg [String]
|
|
194
|
+
# @kwarg [String, Symbol] table
|
|
192
195
|
#
|
|
193
196
|
# @kwarg [Symbol, String] parent
|
|
194
197
|
#
|
|
@@ -203,7 +206,7 @@ module Planter
|
|
|
203
206
|
# class UsersSeeder < Planter::Seeder
|
|
204
207
|
# seeding_method :csv,
|
|
205
208
|
# number_of_records: 2,
|
|
206
|
-
#
|
|
209
|
+
# table: :users
|
|
207
210
|
# parent: :person,
|
|
208
211
|
# csv_name: :awesome_users,
|
|
209
212
|
# unique_columns %i[username email],
|
|
@@ -212,7 +215,7 @@ module Planter
|
|
|
212
215
|
def self.seeding_method(
|
|
213
216
|
seed_method,
|
|
214
217
|
number_of_records: 1,
|
|
215
|
-
|
|
218
|
+
table: nil,
|
|
216
219
|
parent: nil,
|
|
217
220
|
csv_name: nil,
|
|
218
221
|
unique_columns: nil,
|
|
@@ -224,9 +227,9 @@ module Planter
|
|
|
224
227
|
|
|
225
228
|
self.seed_method = seed_method
|
|
226
229
|
self.number_of_records = number_of_records
|
|
227
|
-
self.
|
|
230
|
+
self.table_name = (table || default_table_name).to_s
|
|
228
231
|
self.parent = parent
|
|
229
|
-
self.csv_name = csv_name ||
|
|
232
|
+
self.csv_name = (csv_name || table_name).to_s
|
|
230
233
|
self.erb_trim_mode = erb_trim_mode || Planter.config.erb_trim_mode
|
|
231
234
|
self.unique_columns =
|
|
232
235
|
case unique_columns
|
|
@@ -235,14 +238,22 @@ module Planter
|
|
|
235
238
|
end
|
|
236
239
|
end
|
|
237
240
|
|
|
241
|
+
##
|
|
242
|
+
# Return the table name inferred from the seeder class name.
|
|
243
|
+
#
|
|
244
|
+
# @return [String]
|
|
245
|
+
def self.default_table_name
|
|
246
|
+
to_s.delete_suffix("Seeder").underscore
|
|
247
|
+
end
|
|
248
|
+
|
|
238
249
|
##
|
|
239
250
|
# The default seed method. To use this method, your class must provide a
|
|
240
251
|
# valid +seeding_method+, and not implement its own +seed+ method.
|
|
241
252
|
def seed
|
|
242
253
|
validate_attributes
|
|
243
|
-
extract_data_from_csv if seed_method == :csv
|
|
254
|
+
extract_data_from_csv if context.seed_method == :csv
|
|
244
255
|
|
|
245
|
-
parent ? create_records_from_parent : create_records
|
|
256
|
+
context.parent ? create_records_from_parent : create_records
|
|
246
257
|
end
|
|
247
258
|
|
|
248
259
|
private
|
|
@@ -250,7 +261,7 @@ module Planter
|
|
|
250
261
|
##
|
|
251
262
|
# Creates records from the +data+ attribute.
|
|
252
263
|
def create_records
|
|
253
|
-
number_of_records.times do
|
|
264
|
+
context.number_of_records.times do
|
|
254
265
|
data.each { |record| create_record(record) }
|
|
255
266
|
end
|
|
256
267
|
end
|
|
@@ -258,48 +269,30 @@ module Planter
|
|
|
258
269
|
##
|
|
259
270
|
# Create records from the +data+ attribute for each record in the +parent+.
|
|
260
271
|
def create_records_from_parent
|
|
261
|
-
adapter.parent_ids(
|
|
262
|
-
number_of_records.times do
|
|
272
|
+
adapter.parent_ids(context: context).each do |parent_id|
|
|
273
|
+
context.number_of_records.times do
|
|
263
274
|
data.each { |record| create_record(record, parent_id: parent_id) }
|
|
264
275
|
end
|
|
265
276
|
end
|
|
266
277
|
end
|
|
267
278
|
|
|
268
279
|
def create_record(record, parent_id: nil)
|
|
269
|
-
|
|
270
|
-
unique = unique.merge(foreign_key => parent_id) if parent_id
|
|
271
|
-
unique, attrs = filter_lookup_attributes(unique, attrs)
|
|
280
|
+
lookup_attributes, create_attributes = record_attributes.prepare(record, parent_id: parent_id)
|
|
272
281
|
adapter.create_record(
|
|
273
|
-
|
|
274
|
-
lookup_attributes:
|
|
275
|
-
create_attributes:
|
|
282
|
+
context: context,
|
|
283
|
+
lookup_attributes: lookup_attributes,
|
|
284
|
+
create_attributes: create_attributes
|
|
276
285
|
)
|
|
277
286
|
end
|
|
278
287
|
|
|
279
|
-
def full_csv_name
|
|
280
|
-
@full_csv_name ||=
|
|
281
|
-
%W[#{csv_name}.csv #{csv_name}.csv.erb #{csv_name}.erb.csv]
|
|
282
|
-
.map { |f| Rails.root.join(Planter.config.csv_files_directory, f).to_s }
|
|
283
|
-
.find { |f| ::File.file?(f) }
|
|
284
|
-
end
|
|
285
|
-
|
|
286
288
|
def extract_data_from_csv
|
|
287
|
-
|
|
288
|
-
if full_csv_name.include?(".erb")
|
|
289
|
-
contents = ERB.new(contents, trim_mode: erb_trim_mode).result(binding)
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
@data ||= ::CSV.parse(
|
|
293
|
-
contents,
|
|
294
|
-
headers: true,
|
|
295
|
-
header_converters: :symbol
|
|
296
|
-
).map(&:to_hash)
|
|
289
|
+
@data ||= csv_data_source.data
|
|
297
290
|
end
|
|
298
291
|
|
|
299
292
|
def validate_attributes # :nodoc:
|
|
300
|
-
case seed_method
|
|
293
|
+
case context.seed_method
|
|
301
294
|
when :csv
|
|
302
|
-
raise "Couldn't find csv for #{
|
|
295
|
+
raise "Couldn't find csv for #{context.table_name}" unless csv_data_source.path
|
|
303
296
|
when :data_array
|
|
304
297
|
raise "data is not defined in the seeder" if public_send(:data).nil?
|
|
305
298
|
else
|
|
@@ -307,74 +300,30 @@ module Planter
|
|
|
307
300
|
end
|
|
308
301
|
end
|
|
309
302
|
|
|
310
|
-
def
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
]
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
def transform(field, value, record)
|
|
324
|
-
case transformations[field].arity
|
|
325
|
-
when 0 then transformations[field].call
|
|
326
|
-
when 1 then transformations[field].call(value)
|
|
327
|
-
when 2 then transformations[field].call(value, record)
|
|
328
|
-
end
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
def split_record(rec) # :nodoc:
|
|
332
|
-
return [rec, {}] unless unique_columns
|
|
333
|
-
|
|
334
|
-
u = unique_columns.each_with_object({}) { |c, h| h[c] = rec[c] }
|
|
335
|
-
[u, rec.except(*unique_columns)]
|
|
303
|
+
def context
|
|
304
|
+
@context ||= Planter::SeedContext.new(
|
|
305
|
+
table_name: table_name,
|
|
306
|
+
seed_method: seed_method,
|
|
307
|
+
csv_name: csv_name,
|
|
308
|
+
parent: parent,
|
|
309
|
+
number_of_records: number_of_records,
|
|
310
|
+
unique_columns: unique_columns,
|
|
311
|
+
erb_trim_mode: erb_trim_mode
|
|
312
|
+
)
|
|
336
313
|
end
|
|
337
314
|
|
|
338
|
-
def
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
table_columns = adapter.table_columns(model_name: model).map(&:to_s)
|
|
342
|
-
native_lookup_attributes = lookup_attributes.select do |field, _value|
|
|
343
|
-
table_columns.include?(field.to_s)
|
|
344
|
-
end
|
|
345
|
-
non_column_lookup_attributes = lookup_attributes.except(*native_lookup_attributes.keys)
|
|
346
|
-
|
|
347
|
-
if non_column_lookup_attributes.any?
|
|
348
|
-
warn_non_column_lookup_attributes(non_column_lookup_attributes.keys)
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
if native_lookup_attributes.empty?
|
|
352
|
-
raise "No native lookup columns found for #{model}. " \
|
|
353
|
-
"Add a native table column to the seed data or unique_columns."
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
[
|
|
357
|
-
native_lookup_attributes,
|
|
358
|
-
non_column_lookup_attributes.merge(create_attributes)
|
|
359
|
-
]
|
|
315
|
+
def csv_data_source
|
|
316
|
+
@csv_data_source ||= Planter::CsvDataSource.new(context: context, seeder: self)
|
|
360
317
|
end
|
|
361
318
|
|
|
362
|
-
def
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
@warned_non_column_lookup_attributes << warning_key
|
|
368
|
-
warn(
|
|
369
|
-
"WARNING: Planter moved non-column lookup attributes for #{model} " \
|
|
370
|
-
"into create attributes: #{warning_key.last.join(", ")}"
|
|
319
|
+
def record_attributes
|
|
320
|
+
@record_attributes ||= Planter::RecordAttributes.new(
|
|
321
|
+
context: context,
|
|
322
|
+
adapter: adapter,
|
|
323
|
+
transformations_provider: -> { public_send(:transformations) }
|
|
371
324
|
)
|
|
372
325
|
end
|
|
373
326
|
|
|
374
|
-
def foreign_key
|
|
375
|
-
adapter.foreign_key(model_name: model, parent: parent)
|
|
376
|
-
end
|
|
377
|
-
|
|
378
327
|
def adapter
|
|
379
328
|
Planter.config.adapter
|
|
380
329
|
end
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Planter
|
|
4
|
+
##
|
|
5
|
+
# Performs read-only validation of the configured seed plan.
|
|
6
|
+
#
|
|
7
|
+
# The validator checks that requested seeders can be loaded, that built-in
|
|
8
|
+
# seeding methods have the files or data they need, and that the configured
|
|
9
|
+
# adapter exposes Planter's public adapter API.
|
|
10
|
+
class Validator
|
|
11
|
+
##
|
|
12
|
+
# Structured validation result containing fatal errors and non-fatal
|
|
13
|
+
# warnings.
|
|
14
|
+
class Result
|
|
15
|
+
##
|
|
16
|
+
# Fatal validation errors.
|
|
17
|
+
#
|
|
18
|
+
# @return [Array<String>]
|
|
19
|
+
attr_reader :errors
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# Non-fatal validation warnings.
|
|
23
|
+
#
|
|
24
|
+
# @return [Array<String>]
|
|
25
|
+
attr_reader :warnings
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# Create an empty validation result.
|
|
29
|
+
def initialize
|
|
30
|
+
@errors = []
|
|
31
|
+
@warnings = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Record a fatal validation error.
|
|
36
|
+
#
|
|
37
|
+
# @param message [String]
|
|
38
|
+
def add_error(message)
|
|
39
|
+
errors << message
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Record a non-fatal validation warning.
|
|
44
|
+
#
|
|
45
|
+
# @param message [String]
|
|
46
|
+
def add_warning(message)
|
|
47
|
+
warnings << message
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Whether validation completed without fatal errors.
|
|
52
|
+
#
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def success?
|
|
55
|
+
errors.empty?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# Adapter methods required for Planter's public adapter API.
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<Symbol>]
|
|
63
|
+
REQUIRED_ADAPTER_METHODS = %i[
|
|
64
|
+
create_record
|
|
65
|
+
parent_ids
|
|
66
|
+
foreign_key
|
|
67
|
+
table_columns
|
|
68
|
+
table_names
|
|
69
|
+
].freeze
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Create a validator.
|
|
73
|
+
#
|
|
74
|
+
# @param config [Planter::Config]
|
|
75
|
+
#
|
|
76
|
+
# @param seeders [Array<String>, nil] seeders to validate
|
|
77
|
+
def initialize(config: Planter.config, seeders: nil)
|
|
78
|
+
@config = config
|
|
79
|
+
@seeders = seeders || requested_seeders
|
|
80
|
+
@result = Result.new
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Validate the seed plan without creating, updating, or deleting records.
|
|
85
|
+
#
|
|
86
|
+
# @return [Planter::Validator::Result]
|
|
87
|
+
def validate
|
|
88
|
+
seeders_present = validate_seeders_present
|
|
89
|
+
validate_adapter
|
|
90
|
+
validate_seeders if seeders_present
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
attr_reader :config, :seeders, :result
|
|
97
|
+
|
|
98
|
+
def requested_seeders
|
|
99
|
+
ENV["SEEDERS"]&.split(",") || config.seeders&.map(&:to_s)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_seeders_present
|
|
103
|
+
return true if seeders.present?
|
|
104
|
+
|
|
105
|
+
result.add_error(
|
|
106
|
+
"No seeders configured. Add seeders to config.seeders in " \
|
|
107
|
+
"config/initializers/planter.rb or set SEEDERS."
|
|
108
|
+
)
|
|
109
|
+
false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_adapter
|
|
113
|
+
REQUIRED_ADAPTER_METHODS.each do |method|
|
|
114
|
+
next if adapter.respond_to?(method)
|
|
115
|
+
|
|
116
|
+
result.add_error("Configured adapter must respond to ##{method}.")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def validate_seeders
|
|
121
|
+
seeders.each { |seeder| validate_seeder(seeder) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def validate_seeder(seeder)
|
|
125
|
+
return unless require_seeder(seeder)
|
|
126
|
+
|
|
127
|
+
seeder_class = constantize_seeder(seeder)
|
|
128
|
+
return unless seeder_class
|
|
129
|
+
return unless validate_seeder_class(seeder, seeder_class)
|
|
130
|
+
return if custom_seed?(seeder_class)
|
|
131
|
+
|
|
132
|
+
validate_default_seeder(seeder, seeder_class)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def require_seeder(seeder)
|
|
136
|
+
path = seeder_path(seeder)
|
|
137
|
+
unless ::File.file?(path)
|
|
138
|
+
result.add_error("Seeder file not found for #{seeder}: #{path}.")
|
|
139
|
+
return false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
require path
|
|
143
|
+
true
|
|
144
|
+
rescue LoadError, SyntaxError, StandardError => error
|
|
145
|
+
result.add_error(
|
|
146
|
+
"Could not load seeder file for #{seeder}: #{error.class}: #{error.message}."
|
|
147
|
+
)
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def constantize_seeder(seeder)
|
|
152
|
+
"#{seeder.camelize}Seeder".constantize
|
|
153
|
+
rescue NameError
|
|
154
|
+
result.add_error("Seeder class #{seeder.camelize}Seeder could not be constantized.")
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def validate_seeder_class(seeder, seeder_class)
|
|
159
|
+
return true if seeder_class <= Planter::Seeder
|
|
160
|
+
|
|
161
|
+
result.add_error("#{seeder.camelize}Seeder must inherit from Planter::Seeder.")
|
|
162
|
+
false
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def custom_seed?(seeder_class)
|
|
166
|
+
seeder_class.instance_method(:seed).owner != Planter::Seeder
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def validate_default_seeder(seeder, seeder_class)
|
|
170
|
+
unless Planter::Seeder::SEEDING_METHODS.include?(seeder_class.seed_method&.intern)
|
|
171
|
+
result.add_error("#{seeder.camelize}Seeder must define a valid seeding_method.")
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
case seeder_class.seed_method.intern
|
|
176
|
+
when :csv
|
|
177
|
+
validate_csv_seeder(seeder, seeder_class)
|
|
178
|
+
when :data_array
|
|
179
|
+
validate_data_array_seeder(seeder, seeder_class)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def validate_csv_seeder(seeder, seeder_class)
|
|
184
|
+
seeder_instance = build_seeder(seeder, seeder_class)
|
|
185
|
+
return unless seeder_instance
|
|
186
|
+
|
|
187
|
+
context = context_for(seeder_class)
|
|
188
|
+
data_source = Planter::CsvDataSource.new(context: context, seeder: seeder_instance)
|
|
189
|
+
unless data_source.path
|
|
190
|
+
result.add_error("CSV seed file not found for #{seeder_class.table_name}.")
|
|
191
|
+
return
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
return unless adapter.respond_to?(:table_columns)
|
|
195
|
+
|
|
196
|
+
validate_csv_headers(data_source.path, context, seeder_instance)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def validate_data_array_seeder(seeder, seeder_class)
|
|
200
|
+
seeder_instance = build_seeder(seeder, seeder_class)
|
|
201
|
+
return unless seeder_instance
|
|
202
|
+
|
|
203
|
+
return unless seeder_instance.public_send(:data).nil?
|
|
204
|
+
|
|
205
|
+
result.add_error("#{seeder.camelize}Seeder must define non-nil data.")
|
|
206
|
+
rescue => error
|
|
207
|
+
result.add_error(
|
|
208
|
+
"#{seeder.camelize}Seeder data could not be read: " \
|
|
209
|
+
"#{error.class}: #{error.message}."
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def validate_csv_headers(path, context, seeder_instance)
|
|
214
|
+
headers = csv_headers(path, context, seeder_instance)
|
|
215
|
+
return if headers.nil?
|
|
216
|
+
|
|
217
|
+
table_columns = adapter.table_columns(context: context).map(&:to_s)
|
|
218
|
+
lookup_fields = csv_lookup_fields(headers, context)
|
|
219
|
+
native_lookup_fields = lookup_fields.select do |field|
|
|
220
|
+
table_columns.include?(field.to_s)
|
|
221
|
+
end
|
|
222
|
+
non_column_lookup_fields = lookup_fields - native_lookup_fields
|
|
223
|
+
|
|
224
|
+
if native_lookup_fields.empty?
|
|
225
|
+
result.add_error(
|
|
226
|
+
"No native lookup columns found for #{context.table_name}. " \
|
|
227
|
+
"Add a native table column to the seed data or unique_columns."
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
return if non_column_lookup_fields.empty?
|
|
232
|
+
|
|
233
|
+
result.add_warning(
|
|
234
|
+
"Planter will move non-column lookup attributes for #{context.table_name} " \
|
|
235
|
+
"into create attributes: #{non_column_lookup_fields.map(&:to_s).sort.join(", ")}."
|
|
236
|
+
)
|
|
237
|
+
rescue => error
|
|
238
|
+
result.add_error(
|
|
239
|
+
"CSV headers could not be read for #{context.table_name}: " \
|
|
240
|
+
"#{error.class}: #{error.message}."
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def csv_headers(path, context, seeder_instance)
|
|
245
|
+
header = ::File.foreach(path).first.to_s
|
|
246
|
+
if path.include?(".erb") && header.include?("<%")
|
|
247
|
+
header = ERB.new(header, trim_mode: context.erb_trim_mode).result(
|
|
248
|
+
seeder_instance.instance_eval { binding }
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
::CSV.parse(header, headers: true, header_converters: :symbol).headers
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def csv_lookup_fields(headers, context)
|
|
256
|
+
fields = context.unique_columns || headers
|
|
257
|
+
fields = fields.compact.map(&:to_sym)
|
|
258
|
+
|
|
259
|
+
if context.parent && adapter.respond_to?(:foreign_key)
|
|
260
|
+
fields << adapter.foreign_key(context: context).to_sym
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
fields
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def build_seeder(seeder, seeder_class)
|
|
267
|
+
seeder_class.new
|
|
268
|
+
rescue => error
|
|
269
|
+
result.add_error(
|
|
270
|
+
"#{seeder.camelize}Seeder could not be initialized: " \
|
|
271
|
+
"#{error.class}: #{error.message}."
|
|
272
|
+
)
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def context_for(seeder_class)
|
|
277
|
+
Planter::SeedContext.new(
|
|
278
|
+
table_name: seeder_class.table_name,
|
|
279
|
+
seed_method: seeder_class.seed_method,
|
|
280
|
+
csv_name: seeder_class.csv_name,
|
|
281
|
+
parent: seeder_class.parent,
|
|
282
|
+
number_of_records: seeder_class.number_of_records,
|
|
283
|
+
unique_columns: seeder_class.unique_columns,
|
|
284
|
+
erb_trim_mode: seeder_class.erb_trim_mode
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def seeder_path(seeder)
|
|
289
|
+
Rails.root.join(config.seeders_directory, "#{seeder}_seeder.rb").to_s
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def adapter
|
|
293
|
+
@adapter ||= config.adapter
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
data/lib/planter/version.rb
CHANGED
|
@@ -9,19 +9,19 @@ module Planter
|
|
|
9
9
|
# Major version.
|
|
10
10
|
#
|
|
11
11
|
# @return [Integer]
|
|
12
|
-
MAJOR =
|
|
12
|
+
MAJOR = 1
|
|
13
13
|
|
|
14
14
|
##
|
|
15
15
|
# Minor version.
|
|
16
16
|
#
|
|
17
17
|
# @return [Integer]
|
|
18
|
-
MINOR =
|
|
18
|
+
MINOR = 0
|
|
19
19
|
|
|
20
20
|
##
|
|
21
21
|
# Patch version.
|
|
22
22
|
#
|
|
23
23
|
# @return [Integer]
|
|
24
|
-
PATCH =
|
|
24
|
+
PATCH = 0
|
|
25
25
|
|
|
26
26
|
module_function
|
|
27
27
|
|