planter 0.0.14 → 0.1.3

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 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: