planter 0.4.2 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65e5880dfef8ccce0bf8fec627a74ab7ee76c9d0f84379079056d934b9210e58
4
- data.tar.gz: 2bc7f2d1e88618db03c882291e8dc48b3aa453199f96e235eeab49ec554d0273
3
+ metadata.gz: eb6405a71e2d12a6f934ba4715d5166f1fba3eac5034dd22c1fe0b98f9b3ec07
4
+ data.tar.gz: 4680be7ea150b89db17372e9da80b855e39ca4702bc8067f71fa3b6c6cbc0698
5
5
  SHA512:
6
- metadata.gz: 011cec287611791d0585a80c83e51bd602eafb9bba60755c5079b1eff6f5218ca3a05e21f1651098acda55055590ae4d9ca649f50e3f4559ca7cd971acf647c4
7
- data.tar.gz: 5a11daf5df589cda533596d9039820c0da140f2c2f44eafb40d33a110cbd3aa7fef0a18c78c8c2f7f8d7a4a6dd8ab1f2e3a4d1a0a7a9f3dee3665fece5ff4fd6
6
+ metadata.gz: 22dcb8b7683e1bbcfeb186c62d6250b36b7882271bba2d8e062c845a877225eefe117eef27b28a2c419a1ef860e19ff563c944b04998383b3c6551c075ee0844
7
+ data.tar.gz: 93b858d71f6f32324648d1fcd85604b373bc8f4a69601a445891eb81330b41413afdda8dc5c08526bd45bd1c7601f17ef9b1e93dc50ceb4ed67c05ba69b83921
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Planter
2
2
  [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fevanthegrayt%2Fplanter%2Fbadge%3Fref%3Dmaster&style=flat)](https://actions-badge.atrox.dev/evanthegrayt/planter/goto?ref=master)
3
- [![Gem Version](https://badge.fury.io/rb/planter.svg)](https://badge.fury.io/rb/planter)
4
- ![Language: Ruby](https://img.shields.io/static/v1?label=language&message=Ruby&color=CC342D&style=flat&logo=ruby&logoColor=CC342D)
3
+ [![Language: Ruby](https://img.shields.io/static/v1?label=language&message=Ruby&color=CC342D&style=flat&logo=ruby&logoColor=CC342D)](https://www.ruby-lang.org/)
4
+ [![Gem Version](https://img.shields.io/gem/v/planter.svg)](https://rubygems.org/gems/planter)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
7
  Seeds for Rails applications can get complicated fast, and Rails doesn't provide
@@ -23,7 +23,7 @@ currently a pre-release version, it's recommended to lock it to a specific
23
23
  version, as breaking changes may occur, even at the minor level.
24
24
 
25
25
  ```ruby
26
- gem 'planter', '0.4.2'
26
+ gem 'planter', '0.5.0'
27
27
  ```
28
28
 
29
29
  And then execute:
@@ -99,8 +99,8 @@ Planter.seed
99
99
 
100
100
  To create a users seeder, run `rails generate planter:seeder users`. Usually,
101
101
  seeders seed a specific table, so it's recommended to name your seeders after
102
- the table. If you don't, you'll need to manually specify a few things. More on
103
- that later. This will create a file named `db/seeds/users_seeder.rb` (the
102
+ the table. If you don't, specify the table with the `table` option in
103
+ `seeding_method`. This will create a file named `db/seeds/users_seeder.rb` (the
104
104
  directory will be created if it doesn't exist) with the following contents.
105
105
 
106
106
  ```ruby
@@ -108,7 +108,7 @@ class UsersSeeder < Planter::Seeder
108
108
  # TODO: Choose a seeding_method. For example:
109
109
  # seeding_method :csv
110
110
 
111
- # For now, we overload the seed method so no exception will be raised.
111
+ # For now, we override the seed method so no exception will be raised.
112
112
  def seed
113
113
  end
114
114
  end
@@ -122,15 +122,38 @@ correct order, you'll need to adjust the array manually.
122
122
  - When adjusting the array, always keep the closing bracket on its own line, or
123
123
  the generator won't know where to put the new seeders.
124
124
 
125
+ You can also tell the generator which seeding style to use.
126
+
127
+ ```bash
128
+ rails generate planter:seeder users --seeding-method=csv
129
+ rails generate planter:seeder users --seeding-method=data-array
130
+ rails generate planter:seeder users --seeding-method=custom
131
+ ```
132
+
133
+ `--seeding-method=csv` creates a seeder with `seeding_method :csv` and creates
134
+ `db/seed_files/users.csv` with headers pulled from the `users` table.
135
+ `--seeding-method=data-array` creates a seeder with `seeding_method :data_array`
136
+ and an empty `data` method. `--seeding-method=custom` creates a seeder with an
137
+ empty `seed` method, which is useful when the built-in seeding methods don't
138
+ fit.
139
+
125
140
  If you want to generate a seeder for every table currently in your database, run
126
- `rails generate planter:seeder ALL`.
141
+ `rails generate planter:seeder ALL`. The seeding style options can be used with
142
+ `ALL`, too; for example,
143
+ `rails generate planter:seeder ALL --seeding-method=csv`.
127
144
 
128
145
  Planter uses `Planter::Adapters::ActiveRecord` by default, so the built-in
129
146
  seeding methods work with Active Record models without extra configuration. See
130
147
  [Custom Adapters](#custom-adapters) if you want to use a different persistence
131
148
  backend.
132
149
 
133
- You then need to choose a seeding method, of which there are currently two.
150
+ Each generated seeder needs a seeding method. In practice:
151
+
152
+ - Use `csv` when the seed data should live in a file that is easy to review or
153
+ edit outside Ruby.
154
+ - Use `data_array` when the seed data should be generated dynamically in Ruby.
155
+ - Use a custom `seed` method when the built-in persistence flow is not the right
156
+ fit for the job.
134
157
 
135
158
  ### Seeding from CSV
136
159
  To seed from CSV, you simply need to add the following to your seeder class.
@@ -141,9 +164,11 @@ class UsersSeeder < Planter::Seeder
141
164
  end
142
165
  ```
143
166
 
144
- Then, create a directory called `db/seed_files`, and create a csv file called
145
- `db/seed_files/users.csv`. In this file, the header should be the field names,
146
- and the rest of the rows should be the corresponding data.
167
+ Then, create a directory called `db/seed_files`, and create a CSV file called
168
+ `db/seed_files/users.csv`. You can also run
169
+ `rails generate planter:seeder users --seeding-method=csv` to create this file
170
+ automatically. In this file, the headers should be the field names, and the rest
171
+ of the rows should be the corresponding data.
147
172
 
148
173
  ```
149
174
  email,username
@@ -151,13 +176,51 @@ test1@example.com,test1
151
176
  test2@example.com,test2
152
177
  ```
153
178
 
179
+ For idempotent seeds, pass `unique_columns` to control which columns are used to
180
+ find existing records. This example finds users by email and only uses
181
+ `username` when creating a new record.
182
+
183
+ ```ruby
184
+ class UsersSeeder < Planter::Seeder
185
+ seeding_method :csv, unique_columns: :email
186
+ end
187
+ ```
188
+
189
+ ```
190
+ email,username
191
+ test1@example.com,test1
192
+ test2@example.com,test2
193
+ ```
194
+
195
+ CSV seeders can also be useful for simple model-less tables, such as join
196
+ tables. With the default Active Record adapter, Planter will use the model when
197
+ one exists and fall back to direct table inserts when one does not.
198
+
199
+ ```ruby
200
+ class RolesUsersSeeder < Planter::Seeder
201
+ seeding_method :csv
202
+ end
203
+ ```
204
+
205
+ ```
206
+ user_id,role_id
207
+ 1,1
208
+ 2,1
209
+ ```
210
+
154
211
  If the CSV file is named differently than the seeder, you can specify the
155
212
  `:csv_name` option. Note that the value should not include the file extension.
213
+ If the seeder name does not match the table being seeded, specify the `:table`
214
+ option.
156
215
 
157
216
  ```ruby
158
217
  class UsersSeeder < Planter::Seeder
159
218
  seeding_method :csv, csv_name: :people
160
219
  end
220
+
221
+ class PeopleSeeder < Planter::Seeder
222
+ seeding_method :csv, table: :users
223
+ end
161
224
  ```
162
225
 
163
226
  `ERB` can be used in the CSV files if you end the file name with `.csv.erb` or
@@ -203,7 +266,7 @@ end
203
266
  For help with `erb_trim_mode`, see the help documentation for `ERB::new`.
204
267
 
205
268
  Lastly, it's worth mentioning `transformations` under the CSV section, as that's
206
- usually the pace where they're needed most, but it will work with any method.
269
+ usually the place where they're needed most, but they work with any method.
207
270
 
208
271
  If you're seeding with a CSV, and it contains values that need to have code
209
272
  executed on them before it's imported into the database, you can define an
@@ -264,12 +327,13 @@ the `Planter::Seeder` parent class automatically provides `attr_reader :data`.
264
327
 
265
328
  Running `rails planter:seed` should now seed your `users` table.
266
329
 
267
- You can also seed children records for every existing record of a parent model.
330
+ You can also seed child records for every existing record of a parent relation.
268
331
  For example, to seed an address for every user, you'd need to create an
269
332
  `AddressesSeeder` that uses the `parent` option, as seen below. This option
270
- should be the name of the `belongs_to` association in your model when using the
271
- default Active Record adapter. The primary key, foreign key, and model name of
272
- the parent will all be determined by the adapter.
333
+ is interpreted by the configured adapter. With the default Active Record adapter,
334
+ it should be the name of the `belongs_to` association on a model-backed table.
335
+ The primary key, foreign key, and persistence details will all be determined by
336
+ the adapter.
273
337
 
274
338
  ```ruby
275
339
  require 'faker'
@@ -289,11 +353,13 @@ end
289
353
  ```
290
354
 
291
355
  Note that specifying `number_of_records` in this instance will create that many
292
- records *for each record of the parent model*.
356
+ records *for each record of the parent relation*.
293
357
 
294
358
  ### Custom seeds
295
- To write your own custom seeds, just overload the `seed` method and do whatever
296
- you need to do.
359
+ To write your own custom seeds, generate with `--seeding-method=custom` or
360
+ override the `seed` method and do whatever you need to do. This is the right
361
+ choice when the seed depends on application-specific service objects, multiple
362
+ tables, or logic that does not map cleanly to CSV or `data_array`.
297
363
 
298
364
  ```ruby
299
365
  class UsersSeeder < Planter::Seeder
@@ -312,7 +378,13 @@ end
312
378
  Active Record is the default adapter, but you can provide your own adapter
313
379
  object in the initializer. Replace the generated Active Record adapter require
314
380
  and configuration with your custom adapter, while keeping `config.seeders` as
315
- your ordered seed plan.
381
+ your ordered seed plan. The default adapter creates records through Active
382
+ Record models when they exist, and falls back to direct table inserts for
383
+ model-less tables such as join tables.
384
+
385
+ For a full tutorial, see the
386
+ [Writing a Custom Adapter](https://github.com/evanthegrayt/planter/wiki/Writing-a-Custom-Adapter)
387
+ wiki page.
316
388
 
317
389
  You can generate a custom adapter stub with the following command.
318
390
 
@@ -320,11 +392,8 @@ You can generate a custom adapter stub with the following command.
320
392
  $ rails generate planter:adapter sequel
321
393
  ```
322
394
 
323
- This creates `lib/planter/adapters/sequel.rb` with the required adapter methods
324
- and updates `config/initializers/planter.rb` to use it. The generated methods
325
- raise `NotImplementedError` until you implement them. Running the generator
326
- replaces the currently configured adapter line, but does not change
327
- `config.seeders`.
395
+ This creates `lib/planter/adapters/sequel.rb` with the required adapter methods,
396
+ updates `config/initializers/planter.rb`, and leaves `config.seeders` unchanged.
328
397
 
329
398
  ```ruby
330
399
  require 'planter'
@@ -335,25 +404,26 @@ Planter.configure do |config|
335
404
  end
336
405
  ```
337
406
 
338
- Custom adapters are duck typed. They should implement the same public API as
407
+ Custom adapters are duck typed. They do not need to expose model objects; at
408
+ minimum, they should implement the same table-oriented public API as
339
409
  `Planter::Adapters::ActiveRecord`.
340
410
 
341
411
  ```ruby
342
412
  class MyAdapter
343
- def create_record(model_name:, lookup_attributes:, create_attributes:)
344
- # Find or create a record for model_name.
413
+ def create_record(context:, lookup_attributes:, create_attributes:)
414
+ # Find or create a record for context.table_name.
345
415
  end
346
416
 
347
- def parent_ids(model_name:, parent:)
417
+ def parent_ids(context:)
348
418
  # Return ids for each parent record used by parent seeding.
349
419
  end
350
420
 
351
- def foreign_key(model_name:, parent:)
421
+ def foreign_key(context:)
352
422
  # Return the attribute used to attach a parent id to the seeded record.
353
423
  end
354
424
 
355
- def table_columns(model_name:)
356
- # Return native columns or fields for model_name.
425
+ def table_columns(context:)
426
+ # Return native columns or fields for context.table_name.
357
427
  end
358
428
 
359
429
  def table_names
@@ -372,8 +442,10 @@ issue](https://github.com/evanthegrayt/planter/issues/new). Just make sure
372
442
  the topic doesn't already exist. Better yet, you can always submit a Pull
373
443
  Request.
374
444
 
375
- ## Self-Promotion
376
- I do these projects for fun, and I enjoy knowing that they're helpful to people.
377
- Consider starring [the repository](https://github.com/evanthegrayt/planter)
378
- if you like it! If you love it, follow me [on
379
- GitHub](https://github.com/evanthegrayt)!
445
+ ## Support this project
446
+ I love knowing when people find my work useful. Any kind of support is very much
447
+ appreciated!
448
+
449
+ - ⭐️ Like the project? Star [the repository](https://github.com/evanthegrayt/planter)!
450
+ - ❤️ Love the project? Follow me [on GitHub](https://github.com/evanthegrayt)!
451
+ - 💸 *Really* love it? Consider [buying me a tea](https://paypal.me/evanrgray)!
@@ -1,10 +1,17 @@
1
1
  module Planter
2
+ ##
3
+ # Namespace for Rails generators provided by Planter.
2
4
  module Generators
5
+ ##
6
+ # Rails generator that creates a custom adapter stub and configures Planter
7
+ # to use it.
3
8
  class AdapterGenerator < Rails::Generators::Base
4
9
  argument :adapter, required: true
5
10
 
6
11
  desc "Creates an adapter file at lib/planter/adapters"
7
12
 
13
+ ##
14
+ # Create a custom adapter file with the required adapter API.
8
15
  def generate_adapter
9
16
  create_file adapter_path, <<~RUBY
10
17
  # frozen_string_literal: true
@@ -12,12 +19,12 @@ module Planter
12
19
  module Planter
13
20
  module Adapters
14
21
  ##
15
- # Custom adapter for Planter.
22
+ # Custom table-oriented adapter for Planter.
16
23
  class #{adapter_class_name}
17
24
  ##
18
25
  # Create a record unless one already exists.
19
26
  #
20
- # @param [String] model_name the model being seeded
27
+ # @param [Planter::SeedContext] context seeder configuration
21
28
  #
22
29
  # @param [Hash] lookup_attributes attributes used to find the record
23
30
  #
@@ -25,41 +32,37 @@ module Planter
25
32
  # creating a new record
26
33
  #
27
34
  # @return [Object]
28
- def create_record(model_name:, lookup_attributes:, create_attributes:)
35
+ def create_record(context:, lookup_attributes:, create_attributes:)
29
36
  raise NotImplementedError, "\#{self.class} must implement #create_record"
30
37
  end
31
38
 
32
39
  ##
33
40
  # Return the parent ids to use when seeding child records.
34
41
  #
35
- # @param [String] model_name the model being seeded
36
- #
37
- # @param [String, Symbol] parent the parent association name
42
+ # @param [Planter::SeedContext] context seeder configuration
38
43
  #
39
44
  # @return [Array]
40
- def parent_ids(model_name:, parent:)
45
+ def parent_ids(context:)
41
46
  raise NotImplementedError, "\#{self.class} must implement #parent_ids"
42
47
  end
43
48
 
44
49
  ##
45
50
  # Return the foreign key used to assign a parent id on a child record.
46
51
  #
47
- # @param [String] model_name the model being seeded
48
- #
49
- # @param [String, Symbol] parent the parent association name
52
+ # @param [Planter::SeedContext] context seeder configuration
50
53
  #
51
54
  # @return [String, Symbol]
52
- def foreign_key(model_name:, parent:)
55
+ def foreign_key(context:)
53
56
  raise NotImplementedError, "\#{self.class} must implement #foreign_key"
54
57
  end
55
58
 
56
59
  ##
57
- # Return native columns or fields for the model being seeded.
60
+ # Return native columns or fields for the table being seeded.
58
61
  #
59
- # @param [String] model_name the model being seeded
62
+ # @param [Planter::SeedContext] context seeder configuration
60
63
  #
61
64
  # @return [Array<String>]
62
- def table_columns(model_name:)
65
+ def table_columns(context:)
63
66
  raise NotImplementedError, "\#{self.class} must implement #table_columns"
64
67
  end
65
68
 
@@ -76,6 +79,8 @@ module Planter
76
79
  RUBY
77
80
  end
78
81
 
82
+ ##
83
+ # Update the Planter initializer to require and configure the new adapter.
79
84
  def update_initializer
80
85
  contents = ::File.read(initializer_full_path)
81
86
  contents = replace_or_insert_adapter_require(contents)
@@ -1,8 +1,14 @@
1
1
  module Planter
2
+ ##
3
+ # Namespace for Rails generators provided by Planter.
2
4
  module Generators
5
+ ##
6
+ # Rails generator that creates Planter's initializer.
3
7
  class InitializerGenerator < Rails::Generators::Base
4
- desc "Genrates an initializer for Planter at config/initializers/planter.rb"
8
+ desc "Generates an initializer for Planter at config/initializers/planter.rb"
5
9
 
10
+ ##
11
+ # Create the default Planter initializer file.
6
12
  def create_initializer_file
7
13
  create_file "config/initializers/planter.rb", <<~EOF
8
14
  require 'planter'
@@ -1,10 +1,34 @@
1
1
  module Planter
2
+ ##
3
+ # Namespace for Rails generators provided by Planter.
2
4
  module Generators
5
+ ##
6
+ # Rails generator that creates one or more Planter seeder files.
7
+ #
8
+ # By default, generated seeders include a TODO comment and an empty +seed+
9
+ # method so they do not raise until a seeding method is chosen. Pass
10
+ # +--seeding-method=csv+, +--seeding-method=data-array+, or
11
+ # +--seeding-method=custom+ to generate a seeder for a specific style. The
12
+ # +csv+ method also creates a CSV seed file with headers pulled from the
13
+ # table being seeded.
3
14
  class SeederGenerator < Rails::Generators::Base
15
+ ##
16
+ # Generator-supported seeding method templates.
17
+ #
18
+ # @return [Array<Symbol>]
19
+ SEEDING_METHODS = %i[csv data_array custom].freeze
20
+
4
21
  argument :seeder, required: true
5
22
 
23
+ class_option :seeding_method,
24
+ type: :string,
25
+ desc: "Generate a seeder for a specific method: csv, data-array, or custom"
26
+
6
27
  desc "Creates a seeder file at #{::Planter.config.seeders_directory}"
7
28
 
29
+ ##
30
+ # Generate the requested seeder, or generate a seeder for every table
31
+ # when the argument is +ALL+.
8
32
  def generate_seeders
9
33
  (seeder == "ALL") ? tables.each { |t| generate(t) } : generate(seeder)
10
34
  end
@@ -14,22 +38,98 @@ module Planter
14
38
  def generate(seeder)
15
39
  empty_directory ::Planter.config.seeders_directory
16
40
 
17
- create_file "#{::Planter.config.seeders_directory}/#{seeder}_seeder.rb", <<~EOF
18
- class #{seeder.camelize}Seeder < Planter::Seeder
19
- # TODO: Choose a seeding_method. For example:
20
- # seeding_method :csv
41
+ create_file(
42
+ "#{::Planter.config.seeders_directory}/#{seeder}_seeder.rb",
43
+ [
44
+ "class #{seeder.camelize}Seeder < Planter::Seeder",
45
+ indent(seeder_contents),
46
+ "end",
47
+ ""
48
+ ].join("\n")
49
+ )
21
50
 
22
- # For now, we overload the seed method so no exception will be raised.
23
- def seed
24
- end
25
- end
26
- EOF
51
+ create_csv(seeder) if selected_seeding_method == :csv
27
52
 
28
53
  inject_into_file "config/initializers/planter.rb",
29
54
  " #{seeder}\n",
30
55
  before: /^\s*\]\s*$/
31
56
  end
32
57
 
58
+ def seeder_contents
59
+ case selected_seeding_method
60
+ when :csv
61
+ "seeding_method :csv"
62
+ when :data_array
63
+ <<~RUBY.rstrip
64
+ seeding_method :data_array
65
+
66
+ def data
67
+ [
68
+ ]
69
+ end
70
+ RUBY
71
+ when :custom
72
+ custom_seed_contents
73
+ else
74
+ [
75
+ "# TODO: Choose a seeding_method. For example:",
76
+ "# seeding_method :csv",
77
+ "",
78
+ "# For now, we override the seed method so no exception will be raised.",
79
+ custom_seed_contents
80
+ ].join("\n")
81
+ end
82
+ end
83
+
84
+ def custom_seed_contents
85
+ <<~RUBY.rstrip
86
+ def seed
87
+ end
88
+ RUBY
89
+ end
90
+
91
+ def indent(contents)
92
+ contents.lines.map do |line|
93
+ line.strip.empty? ? line : " #{line}"
94
+ end.join
95
+ end
96
+
97
+ def selected_seeding_method
98
+ return unless options["seeding_method"]
99
+
100
+ method = options["seeding_method"].tr("-", "_").to_sym
101
+ unless SEEDING_METHODS.include?(method)
102
+ raise Thor::Error, "Expected --seeding-method to be one of: csv, data-array, custom"
103
+ end
104
+
105
+ method
106
+ end
107
+
108
+ def create_csv(seeder)
109
+ empty_directory ::Planter.config.csv_files_directory
110
+
111
+ create_file(
112
+ "#{::Planter.config.csv_files_directory}/#{seeder}.csv",
113
+ "#{csv_headers(seeder).join(",")}\n"
114
+ )
115
+ end
116
+
117
+ def csv_headers(seeder)
118
+ ::Planter.config.adapter.table_columns(context: csv_context(seeder))
119
+ end
120
+
121
+ def csv_context(seeder)
122
+ ::Planter::SeedContext.new(
123
+ table_name: seeder,
124
+ seed_method: :csv,
125
+ csv_name: seeder,
126
+ parent: nil,
127
+ number_of_records: 1,
128
+ unique_columns: nil,
129
+ erb_trim_mode: ::Planter.config.erb_trim_mode
130
+ )
131
+ end
132
+
33
133
  def tables
34
134
  @tables ||= ::Planter.config.adapter.table_names
35
135
  end