planter 0.0.14 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65eb2897cfca2aa456d13a8044d6a86ed9b898aced01a3718e4d11f5dc718da1
4
- data.tar.gz: 2dec806f05018f1bdf1e81282047dba3f049274cf6397aeaa8db55ff7bf8856c
3
+ metadata.gz: a542b8919dda44137996b078e0ac3f773fc8b85bed2225193de3f2a36bdfe099
4
+ data.tar.gz: '058271874209001fd8de03630c2e3f162959621769225bb354060f1ed8848354'
5
5
  SHA512:
6
- metadata.gz: 752f29be613b3ce52fad80262ec500db79176329ac802c44f582375be3304c2435653fea1698ac1475f7da938250833f6807fa4b98f558806594db331024dc21
7
- data.tar.gz: 46c6632d2faed5ab3ee96a4084de5043dd4312048cd1299dbb45910b537e0f5376b087f80e0e97f5ebc041334876cf47f048bbc8df97e6b052489e5c4bccd8d4
6
+ metadata.gz: c9728b99a23877a7f96f4267b963dd7f4e15f4c385303759476937f7f3f33bd6335ec0e6b89fde733ecfd7f5380386ba86ca26bfbdb9ac70f587b8a05efc8812
7
+ data.tar.gz: 1af8f9af55ca77f91f2ef7c01b8cb2c1919166be12f8785d779137d68237050b1ab259726da9546dea7dd2184f85431930e9ec8785d269c16ee1ba6add884b92
data/README.md CHANGED
@@ -21,10 +21,10 @@ You can view the documentation [here](https://evanthegrayt.github.io/planter/).
21
21
  ## Installation
22
22
  Add the following line to your application's Gemfile. Because this plugin is
23
23
  currently a pre-release version, it's recommended to lock it to a specific
24
- version, as breaking changes may occur, even at the patch level.
24
+ version, as breaking changes may occur, even at the minor level.
25
25
 
26
26
  ```ruby
27
- gem 'planter', '0.0.10'
27
+ gem 'planter', '0.1.3'
28
28
  ```
29
29
 
30
30
  And then execute:
@@ -83,6 +83,7 @@ allows you to use `db:seed` for other purposes. If you want Planter to hook
83
83
  into the existing `db:seed` task, simply add the following to `db/seeds.rb`.
84
84
 
85
85
  ```ruby
86
+ # db/seeds.rb
86
87
  Planter.seed
87
88
  ```
88
89
 
@@ -144,8 +145,8 @@ class UsersSeeder < Planter::Seeder
144
145
  end
145
146
  ```
146
147
 
147
- `ERB` can be used in the CSV files if you end the file name with `.csv.erb`.
148
- For example, `users.csv.erb`.
148
+ `ERB` can be used in the CSV files if you end the file name with `.csv.erb` or
149
+ `.erb.csv`. For example, `users.csv.erb`.
149
150
 
150
151
  ```
151
152
  participant_id,name
@@ -175,6 +176,38 @@ end
175
176
 
176
177
  For help with `erb_trim_mode`, see the help documentation for `ERB::new`.
177
178
 
179
+ Lastly, it's worth mentioning `transformations` under the CSV section, as that's
180
+ usually the pace where they're needed most, but it will work with any method.
181
+
182
+ If you're seeding with a CSV, and it contains values that need to have code
183
+ executed on them before it's imported into the database, you can define an
184
+ instance variable called `@transformations`, or a method called
185
+ `transformations`, that returns a Hash of field names, and Procs to run on the
186
+ value. For example, if you have an `admin` column, and the CSV contains "true",
187
+ it will come through as a String, but you probably want it to be a Boolean. This
188
+ can be solved with the following.
189
+
190
+ ```ruby
191
+ class UsersSeeder < Planter::Seeder
192
+ seeding_method :csv
193
+
194
+ def transformations
195
+ {
196
+ admin: ->(value) { value == 'true' },
197
+ last_name: ->(value, row) { "#{value} #{row[:suffix]}".squish }
198
+ }
199
+ end
200
+ end
201
+ ```
202
+
203
+ When defining a Proc/Lambda, you can make it accept 0, 1, or 2 arguments.
204
+ - When `0`, the value is replaced by the result of the Lambda
205
+ - When `1`, the value is passed to the Lambda, and is subsequently replaced by
206
+ the result of the Lambda
207
+ - When `2`, the value is the first argument, and the entire row, as a Hash, is
208
+ the second argument. This allows for more complicated transformations that can
209
+ be dependent on other fields and values in the record.
210
+
178
211
  Running `rails planter:seed` will now seed your `users` table.
179
212
 
180
213
  ## Seeding from a data array
@@ -207,13 +240,16 @@ Running `rails planter:seed` should now seed your `users` table.
207
240
 
208
241
  You can also seed children records for every existing record of a parent model.
209
242
  For example, to seed an address for every user, you'd need to create an
210
- `AddressesSeeder` that uses the `parent_model` option, as seen below.
243
+ `AddressesSeeder` that uses the `parent` option, as seen below. This option
244
+ should be the name of the `belongs_to` association in your model. The primary
245
+ key, foreign key, and model name of the parent will all be determined by
246
+ reflecting on the association.
211
247
 
212
248
  ```ruby
213
249
  require 'faker'
214
250
 
215
251
  class AddressesSeeder < Planter::Seeder
216
- seeding_method :data_array, parent_model: 'User'
252
+ seeding_method :data_array, parent: :user
217
253
 
218
254
  def data
219
255
  [{
@@ -227,9 +263,7 @@ end
227
263
  ```
228
264
 
229
265
  Note that specifying `number_of_records` in this instance will create that many
230
- records *for each record of the parent model*. You can also specify the
231
- association if it's different from the table name, using the `:assocation`
232
- option.
266
+ records *for each record of the parent model*.
233
267
 
234
268
  ### Custom seeds
235
269
  To write your own custom seeds, just overload the `seed` method and do whatever
@@ -26,7 +26,7 @@ module Planter
26
26
  # Another way to seed is to create records from a data array. To do this,
27
27
  # your class must implement a +data+ attribute or method, which is an array
28
28
  # of hashes. Note that this class already provides the +attr_reader+ for this
29
- # attribute, so the most you have to do it create instance variables in your
29
+ # attribute, so the most you have to do is create instance variables in your
30
30
  # constructor. If if you want your data to be different for each new record
31
31
  # (via Faker, +Array#sample+, etc.), you'll probably want to supply a method
32
32
  # called data that returns an array of new data each time.
@@ -38,15 +38,17 @@ module Planter
38
38
  # end
39
39
  # end
40
40
  #
41
- # In both of the above methods, you can specify +parent_model+ and
42
- # +association+. If specified, records will be created via that parent
43
- # model's association. If +association+ is not provided, it will be assumed
44
- # to be the model name, pluralized and snake-cased (implying a +has_many+
45
- # relationship). For example, if we're seeding the users table, and the
46
- # model is +User+, the association will default to +users+.
41
+ # In both of the above methods, you can specify a +parent+ association, which
42
+ # is the +belongs_to+ association name in your model, which, when specified,
43
+ # records will be created for each record in the parent table. For example,
44
+ # if we're seeding the users table, and the model is +User+, which belongs to
45
+ # +Person+, then doing the following will create a user record for each
46
+ # record in the Person table. Note that nothing is automatically done to
47
+ # prevent any validation errors; you must do this on your own, mostly likely
48
+ # using +Faker+ or a similar library.
47
49
  # require 'planter'
48
50
  # class UsersSeeder < Planter::Seeder
49
- # seeding_method :data_array, parent_model: 'Person', association: :users
51
+ # seeding_method :data_array, parent: :person
50
52
  # def data
51
53
  # [{foo: 'bar', baz: 'bar'}]
52
54
  # end
@@ -54,9 +56,8 @@ module Planter
54
56
  #
55
57
  # You can also set +number_of_records+ to determine how many times each
56
58
  # record in the +data+ array will get created. The default is 1. Note that if
57
- # this attribute is set alongside +parent_model+ and +association+,
58
- # +number_of_records+ will be how many records will be created for each
59
- # record in the parent table.
59
+ # this attribute is set alongside +parent+, +number_of_records+ will be how
60
+ # many records will be created for each record in the parent table.
60
61
  # require 'planter'
61
62
  # class UsersSeeder < Planter::Seeder
62
63
  # seeding_method :data_array, number_of_records: 5
@@ -101,6 +102,35 @@ module Planter
101
102
  # @return [Array]
102
103
  attr_reader :data
103
104
 
105
+ ##
106
+ # A hash of user-defined column names and procs to be run on values. This
107
+ # is most useful for when seeding from csv, and you need to transform, say,
108
+ # 'true' (String) into true (Boolean). The user may define this as an
109
+ # instance variable, or define a method that returns the hash.
110
+ #
111
+ # When defining a Proc/Lambda, you can make it accept 0, 1, or 2 arguments.
112
+ # - When 0, the value is replaced by the result of the Lambda.
113
+ # - When 1, the value is passed to the Lambda, and is subsequently
114
+ # replaced by the result of the Lambda.
115
+ # - When 2, the value is the first argument, and the entire row, as a
116
+ # Hash, is the second argument. This allows for more complicated
117
+ # transformations that can be dependent on other fields and values in the
118
+ # record.
119
+ #
120
+ # @return [Hash, nil]
121
+ #
122
+ # @example
123
+ # class UsersSeeder < Planter::Seeder
124
+ # seeding_method :csv
125
+ # def transformations
126
+ # {
127
+ # admin: ->(v) { v == 'true' },
128
+ # last_name: ->(value, row) { "#{value} #{row[:suffix]}".squish }
129
+ # }
130
+ # end
131
+ # end
132
+ attr_reader :transformations
133
+
104
134
  ##
105
135
  # What trim mode should ERB use?
106
136
  #
@@ -127,7 +157,7 @@ module Planter
127
157
  # class must set this attribute via +seeding_method+.
128
158
  #
129
159
  # @return [String]
130
- class_attribute :parent_model
160
+ class_attribute :parent
131
161
 
132
162
  ##
133
163
  # The number of records to create from each record in the +data+ array. If
@@ -137,18 +167,11 @@ module Planter
137
167
  # @return [Integer]
138
168
  class_attribute :number_of_records
139
169
 
140
- ##
141
- # When using +parent_model+, the association name. Your class can set this
142
- # attribute via +seeding_method+.
143
- #
144
- # @return [Symbol]
145
- class_attribute :association
146
-
147
170
  ##
148
171
  # The csv file corresponding to the model.
149
172
  #
150
173
  # @return [String]
151
- class_attribute :csv_file
174
+ class_attribute :csv_name
152
175
 
153
176
  ##
154
177
  # The seeding method specified.
@@ -157,96 +180,59 @@ module Planter
157
180
  class_attribute :seed_method
158
181
 
159
182
  ##
160
- # Access the metaclass so we can define public and private class methods.
161
- class << self
162
- ##
163
- # If your class is going to use the inherited +seed+ method, you must tell
164
- # it which +seeding_method+ to use. The argument to this method must be
165
- # included in the +SEEDING_METHODS+ array.
166
- #
167
- # @param [Symbol] seeding_method
168
- #
169
- # @kwarg [Integer] number_of_records
170
- #
171
- # @kwarg [String] model
172
- #
173
- # @kwarg [String] parent_model
174
- #
175
- # @kwarg [Symbol, String] association
176
- #
177
- # @kwarg [Symbol, String] csv_name
178
- #
179
- # @kwarg [Symbol, String] unique_columns
180
- #
181
- # @kwarg [String] erb_trim_mode
182
- #
183
- # @example
184
- # require 'planter'
185
- # class UsersSeeder < Planter::Seeder
186
- # seeding_method :csv,
187
- # number_of_records: 2,
188
- # model: 'User'
189
- # parent_model: 'Person',
190
- # association: :users,
191
- # csv_name: :awesome_users,
192
- # unique_columns %i[username email],
193
- # erb_trim_mode: '<>'
194
- # end
195
- def seeding_method(
196
- method,
197
- number_of_records: 1,
198
- model: nil,
199
- parent_model: nil,
200
- association: nil,
201
- csv_name: nil,
202
- unique_columns: nil,
203
- erb_trim_mode: nil
204
- )
205
- if !SEEDING_METHODS.include?(method.intern)
206
- raise ArgumentError, "Method must be: #{SEEDING_METHODS.join(', ')}"
207
- elsif association && !parent_model
208
- raise ArgumentError, "Must specify :parent_model with :association"
209
- end
210
-
211
- self.seed_method = method
212
- self.number_of_records = number_of_records
213
- self.model = model || to_s.delete_suffix('Seeder').singularize
214
- self.parent_model = parent_model
215
- self.association = parent_model && (association || determine_association)
216
- self.csv_file = determine_csv_filename(csv_name) if self.seed_method == :csv
217
- self.erb_trim_mode = erb_trim_mode || Planter.config.erb_trim_mode
218
- self.unique_columns =
219
- case unique_columns
220
- when String, Symbol then [unique_columns.intern]
221
- when Array then unique_columns.map(&:intern)
222
- end
223
- end
224
-
225
- private
226
-
227
- def determine_association # :nodoc:
228
- associations =
229
- parent_model.constantize.reflect_on_all_associations.map(&:name)
230
- table = to_s.delete_suffix('Seeder').underscore.split('/').last
231
-
232
- [table, table.singularize].map(&:intern).each do |t|
233
- return t if associations.include?(t)
234
- end
235
-
236
- raise ArgumentError, "Couldn't determine association name"
183
+ # If your class is going to use the inherited +seed+ method, you must tell
184
+ # it which +seeding_method+ to use. The argument to this method must be
185
+ # included in the +SEEDING_METHODS+ array.
186
+ #
187
+ # @param [Symbol] seed_method
188
+ #
189
+ # @kwarg [Integer] number_of_records
190
+ #
191
+ # @kwarg [String] model
192
+ #
193
+ # @kwarg [Symbol, String] parent
194
+ #
195
+ # @kwarg [Symbol, String] csv_name
196
+ #
197
+ # @kwarg [Symbol, String] unique_columns
198
+ #
199
+ # @kwarg [String] erb_trim_mode
200
+ #
201
+ # @example
202
+ # require 'planter'
203
+ # class UsersSeeder < Planter::Seeder
204
+ # seeding_method :csv,
205
+ # number_of_records: 2,
206
+ # model: 'User'
207
+ # parent: :person,
208
+ # csv_name: :awesome_users,
209
+ # unique_columns %i[username email],
210
+ # erb_trim_mode: '<>'
211
+ # end
212
+ def self.seeding_method(
213
+ seed_method,
214
+ number_of_records: 1,
215
+ model: nil,
216
+ parent: nil,
217
+ csv_name: nil,
218
+ unique_columns: nil,
219
+ erb_trim_mode: nil
220
+ )
221
+ unless SEEDING_METHODS.include?(seed_method.intern)
222
+ raise ArgumentError, "Method must be: #{SEEDING_METHODS.join(', ')}"
237
223
  end
238
224
 
239
- def determine_csv_filename(csv_name) # :nodoc:
240
- file = (
241
- csv_name || "#{to_s.delete_suffix('Seeder').underscore}"
242
- ).to_s + '.csv'
243
- [file, "#{file}.erb"].each do |f|
244
- fname = Rails.root.join(Planter.config.csv_files_directory, f).to_s
245
- return fname if ::File.file?(fname)
225
+ self.seed_method = seed_method
226
+ self.number_of_records = number_of_records
227
+ self.model = model || to_s.delete_suffix('Seeder').singularize
228
+ self.parent = parent
229
+ self.csv_name = csv_name || to_s.delete_suffix('Seeder').underscore
230
+ self.erb_trim_mode = erb_trim_mode || Planter.config.erb_trim_mode
231
+ self.unique_columns =
232
+ case unique_columns
233
+ when String, Symbol then [unique_columns.intern]
234
+ when Array then unique_columns.map(&:intern)
246
235
  end
247
-
248
- raise ArgumentError, "Couldn't find csv for #{model}"
249
- end
250
236
  end
251
237
 
252
238
  ##
@@ -254,78 +240,113 @@ module Planter
254
240
  # valid +seeding_method+, and not implement its own +seed+ method.
255
241
  def seed
256
242
  validate_attributes
243
+ extract_data_from_csv if seed_method == :csv
257
244
 
258
- parent_model ? create_records_from_parent : create_records
245
+ parent ? create_records_from_parent : create_records
259
246
  end
260
247
 
261
- protected
248
+ private
262
249
 
263
250
  ##
264
251
  # Creates records from the +data+ attribute.
265
252
  def create_records
266
- data.each do |rec|
267
- number_of_records.times do
268
- rec.transform_values { |value| value == 'NULL' ? nil : value }
269
- unique, attrs = split_record(rec)
270
- model.constantize.where(unique).first_or_create!(attrs)
271
- end
272
- end
253
+ data.each { |record| create_record(record) }
273
254
  end
274
255
 
275
256
  ##
276
- # Create records from the +data+ attribute for each record in the
277
- # +parent_table+, via the specified +association+.
257
+ # Create records from the +data+ attribute for each record in the +parent+.
278
258
  def create_records_from_parent
279
- parent_model.constantize.all.each do |assoc_rec|
280
- number_of_records.times do
281
- data.each { |rec| send(create_method, assoc_rec, association, rec) }
282
- end
259
+ parent_model.constantize.pluck(primary_key).each do |parent_id|
260
+ data.each { |record| create_record(record, parent_id: parent_id) }
283
261
  end
284
262
  end
285
263
 
286
- private
287
-
288
- def create_method # :nodoc:
289
- parent_model.constantize.reflect_on_association(
290
- association
291
- ).macro.to_s.include?('many') ? :create_has_many : :create_has_one
292
- end
293
-
294
- def create_has_many(assoc_rec, association, rec) # :nodoc:
295
- unique, attrs = split_record(rec)
296
- assoc_rec.public_send(association).where(unique).first_or_create!(attrs)
297
- end
298
-
299
- def create_has_one(assoc_rec, association, rec) # :nodoc:
300
- if assoc_rec.public_send(association)
301
- assoc_rec.public_send(association).update_attributes(rec)
302
- else
303
- assoc_rec.public_send("create_#{association}", rec)
264
+ def create_record(record, parent_id: nil)
265
+ number_of_records.times do
266
+ unique, attrs = split_record(apply_transformations(record))
267
+ model.constantize.where(
268
+ unique.tap { |u| u[foreign_key] = parent_id if parent_id }
269
+ ).first_or_create!(attrs)
304
270
  end
305
271
  end
306
272
 
307
273
  def validate_attributes # :nodoc:
308
274
  case seed_method.intern
309
275
  when :csv
310
- contents = ::File.read(csv_file)
311
- if csv_file.end_with?('.erb')
312
- contents = ERB.new(contents, trim_mode: erb_trim_mode).result(binding)
313
- end
314
-
315
- @data ||= ::CSV.parse(
316
- contents, headers: true, header_converters: :symbol
317
- ).map(&:to_hash)
276
+ raise "Couldn't find csv for #{model}" unless full_csv_name
318
277
  when :data_array
319
- raise "Must define '@data'" if public_send(:data).nil?
278
+ raise 'data is not defined in the seeder' if public_send(:data).nil?
320
279
  else
321
- raise("Must set 'seeding_method'")
280
+ raise 'seeding_method not defined in the seeder'
281
+ end
282
+ end
283
+
284
+ def apply_transformations(record)
285
+ return record if public_send(:transformations).nil?
286
+
287
+ Hash[record.map { |field, value| map_record(field, value, record) }]
288
+ end
289
+
290
+ def map_record(field, value, record)
291
+ [
292
+ field,
293
+ transformations.key?(field) ? transform(field, value, record) : value
294
+ ]
295
+ end
296
+
297
+ def transform(field, value, record)
298
+ case transformations[field].arity
299
+ when 0 then transformations[field].call
300
+ when 1 then transformations[field].call(value)
301
+ when 2 then transformations[field].call(value, record)
322
302
  end
323
303
  end
324
304
 
325
305
  def split_record(rec) # :nodoc:
326
306
  return [rec, {}] unless unique_columns
307
+
327
308
  u = unique_columns.each_with_object({}) { |c, h| h[c] = rec.delete(c) }
328
309
  [u, rec]
329
310
  end
311
+
312
+ def association_options
313
+ @association_options ||=
314
+ model.constantize.reflect_on_association(parent).options
315
+ end
316
+
317
+ def primary_key
318
+ @primary_key ||=
319
+ association_options.fetch(:primary_key, :id)
320
+ end
321
+
322
+ def foreign_key
323
+ @foreign_key ||=
324
+ association_options.fetch(:foreign_key, "#{parent}_id")
325
+ end
326
+
327
+ def parent_model
328
+ @parent_model ||=
329
+ association_options.fetch(:class_name, parent.to_s.classify)
330
+ end
331
+
332
+ def full_csv_name
333
+ @full_csv_name ||=
334
+ %W[#{csv_name}.csv #{csv_name}.csv.erb #{csv_name}.erb.csv]
335
+ .map { |f| Rails.root.join(Planter.config.csv_files_directory, f).to_s }
336
+ .find { |f| ::File.file?(f) }
337
+ end
338
+
339
+ def extract_data_from_csv
340
+ contents = ::File.read(full_csv_name)
341
+ if full_csv_name.include?('.erb')
342
+ contents = ERB.new(contents, trim_mode: erb_trim_mode).result(binding)
343
+ end
344
+
345
+ @data ||= ::CSV.parse(
346
+ contents,
347
+ headers: true,
348
+ header_converters: :symbol
349
+ ).map(&:to_hash)
350
+ end
330
351
  end
331
352
  end
@@ -15,13 +15,13 @@ module Planter
15
15
  # Minor version.
16
16
  #
17
17
  # @return [Integer]
18
- MINOR = 0
18
+ MINOR = 1
19
19
 
20
20
  ##
21
21
  # Patch version.
22
22
  #
23
23
  # @return [Integer]
24
- PATCH = 14
24
+ PATCH = 3
25
25
 
26
26
  ##
27
27
  # Version as +[MAJOR, MINOR, PATCH]+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: planter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.14
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Gray
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-27 00:00:00.000000000 Z
11
+ date: 2022-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,20 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 6.1.3
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: 6.1.3.1
19
+ version: 6.1.4.4
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - "~>"
28
25
  - !ruby/object:Gem::Version
29
- version: 6.1.3
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: 6.1.3.1
26
+ version: 6.1.4.4
33
27
  description: Create a seeder for each table in your database, and easily seed from
34
28
  CSV or custom methods
35
29
  email: