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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65e5880dfef8ccce0bf8fec627a74ab7ee76c9d0f84379079056d934b9210e58
4
- data.tar.gz: 2bc7f2d1e88618db03c882291e8dc48b3aa453199f96e235eeab49ec554d0273
3
+ metadata.gz: b66f9b16f720b193d4dee6e0a23382b22a073982634bccbad2d52092516090eb
4
+ data.tar.gz: 5d66d7905379b7b148586b52b85afb2d715501543e88f0d84a88973b734d9f6b
5
5
  SHA512:
6
- metadata.gz: 011cec287611791d0585a80c83e51bd602eafb9bba60755c5079b1eff6f5218ca3a05e21f1651098acda55055590ae4d9ca649f50e3f4559ca7cd971acf647c4
7
- data.tar.gz: 5a11daf5df589cda533596d9039820c0da140f2c2f44eafb40d33a110cbd3aa7fef0a18c78c8c2f7f8d7a4a6dd8ab1f2e3a4d1a0a7a9f3dee3665fece5ff4fd6
6
+ metadata.gz: dde579bb6c055ce0c819148d1d818b39bf44ef9e75c692799dd760c7f9a96a0828d959f3204e6741fa55c41a6b9c2d8045f746142c139b5f20f1ea450ba7b259
7
+ data.tar.gz: 9f208c04796ae9f80ce6d46b4ec94e56da4fc2c62c36abf3198d806621bf2c7d79d9f52fd2d30ad0ee73c94206e7f0997b953ff93dbc93b83ff8064ee7ab85ed
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
@@ -18,12 +18,10 @@ Features include:
18
18
  You can view the documentation [here](https://evanthegrayt.github.io/planter/).
19
19
 
20
20
  ## Installation
21
- Add the following line to your application's Gemfile. Because this plugin is
22
- currently a pre-release version, it's recommended to lock it to a specific
23
- version, as breaking changes may occur, even at the minor level.
21
+ Add the following line to your application's Gemfile.
24
22
 
25
23
  ```ruby
26
- gem 'planter', '0.4.2'
24
+ gem 'planter'
27
25
  ```
28
26
 
29
27
  And then execute:
@@ -97,10 +95,29 @@ into the existing `db:seed` task, simply add the following to `db/seeds.rb`.
97
95
  Planter.seed
98
96
  ```
99
97
 
98
+ Before seeding, you can validate your Planter configuration without creating
99
+ records.
100
+
101
+ ```bash
102
+ rails planter:validate
103
+ ```
104
+
105
+ This command loads the Rails environment, checks the configured seeders, and
106
+ verifies that the configured adapter exposes Planter's adapter API. It prints a
107
+ success message when no issues are found, warnings for non-fatal issues, and
108
+ errors before exiting non-zero when the seed plan is not valid.
109
+
110
+ Like `planter:seed`, validation supports the `SEEDERS` environment variable if
111
+ you want to check only part of the configured plan.
112
+
113
+ ```bash
114
+ rails planter:validate SEEDERS=users,addresses
115
+ ```
116
+
100
117
  To create a users seeder, run `rails generate planter:seeder users`. Usually,
101
118
  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
119
+ the table. If you don't, specify the table with the `table` option in
120
+ `seeding_method`. This will create a file named `db/seeds/users_seeder.rb` (the
104
121
  directory will be created if it doesn't exist) with the following contents.
105
122
 
106
123
  ```ruby
@@ -108,7 +125,7 @@ class UsersSeeder < Planter::Seeder
108
125
  # TODO: Choose a seeding_method. For example:
109
126
  # seeding_method :csv
110
127
 
111
- # For now, we overload the seed method so no exception will be raised.
128
+ # For now, we override the seed method so no exception will be raised.
112
129
  def seed
113
130
  end
114
131
  end
@@ -122,15 +139,38 @@ correct order, you'll need to adjust the array manually.
122
139
  - When adjusting the array, always keep the closing bracket on its own line, or
123
140
  the generator won't know where to put the new seeders.
124
141
 
142
+ You can also tell the generator which seeding style to use.
143
+
144
+ ```bash
145
+ rails generate planter:seeder users --seeding-method=csv
146
+ rails generate planter:seeder users --seeding-method=data-array
147
+ rails generate planter:seeder users --seeding-method=custom
148
+ ```
149
+
150
+ `--seeding-method=csv` creates a seeder with `seeding_method :csv` and creates
151
+ `db/seed_files/users.csv` with headers pulled from the `users` table.
152
+ `--seeding-method=data-array` creates a seeder with `seeding_method :data_array`
153
+ and an empty `data` method. `--seeding-method=custom` creates a seeder with an
154
+ empty `seed` method, which is useful when the built-in seeding methods don't
155
+ fit.
156
+
125
157
  If you want to generate a seeder for every table currently in your database, run
126
- `rails generate planter:seeder ALL`.
158
+ `rails generate planter:seeder ALL`. The seeding style options can be used with
159
+ `ALL`, too; for example,
160
+ `rails generate planter:seeder ALL --seeding-method=csv`.
127
161
 
128
162
  Planter uses `Planter::Adapters::ActiveRecord` by default, so the built-in
129
163
  seeding methods work with Active Record models without extra configuration. See
130
164
  [Custom Adapters](#custom-adapters) if you want to use a different persistence
131
165
  backend.
132
166
 
133
- You then need to choose a seeding method, of which there are currently two.
167
+ Each generated seeder needs a seeding method. In practice:
168
+
169
+ - Use `csv` when the seed data should live in a file that is easy to review or
170
+ edit outside Ruby.
171
+ - Use `data_array` when the seed data should be generated dynamically in Ruby.
172
+ - Use a custom `seed` method when the built-in persistence flow is not the right
173
+ fit for the job.
134
174
 
135
175
  ### Seeding from CSV
136
176
  To seed from CSV, you simply need to add the following to your seeder class.
@@ -141,9 +181,27 @@ class UsersSeeder < Planter::Seeder
141
181
  end
142
182
  ```
143
183
 
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.
184
+ Then, create a directory called `db/seed_files`, and create a CSV file called
185
+ `db/seed_files/users.csv`. You can also run
186
+ `rails generate planter:seeder users --seeding-method=csv` to create this file
187
+ automatically. In this file, the headers should be the field names, and the rest
188
+ of the rows should be the corresponding data.
189
+
190
+ ```
191
+ email,username
192
+ test1@example.com,test1
193
+ test2@example.com,test2
194
+ ```
195
+
196
+ For idempotent seeds, pass `unique_columns` to control which columns are used to
197
+ find existing records. This example finds users by email and only uses
198
+ `username` when creating a new record.
199
+
200
+ ```ruby
201
+ class UsersSeeder < Planter::Seeder
202
+ seeding_method :csv, unique_columns: :email
203
+ end
204
+ ```
147
205
 
148
206
  ```
149
207
  email,username
@@ -151,13 +209,35 @@ test1@example.com,test1
151
209
  test2@example.com,test2
152
210
  ```
153
211
 
212
+ CSV seeders can also be useful for simple model-less tables, such as join
213
+ tables. With the default Active Record adapter, Planter will use the model when
214
+ one exists and fall back to direct table inserts when one does not.
215
+
216
+ ```ruby
217
+ class RolesUsersSeeder < Planter::Seeder
218
+ seeding_method :csv
219
+ end
220
+ ```
221
+
222
+ ```
223
+ user_id,role_id
224
+ 1,1
225
+ 2,1
226
+ ```
227
+
154
228
  If the CSV file is named differently than the seeder, you can specify the
155
229
  `:csv_name` option. Note that the value should not include the file extension.
230
+ If the seeder name does not match the table being seeded, specify the `:table`
231
+ option.
156
232
 
157
233
  ```ruby
158
234
  class UsersSeeder < Planter::Seeder
159
235
  seeding_method :csv, csv_name: :people
160
236
  end
237
+
238
+ class PeopleSeeder < Planter::Seeder
239
+ seeding_method :csv, table: :users
240
+ end
161
241
  ```
162
242
 
163
243
  `ERB` can be used in the CSV files if you end the file name with `.csv.erb` or
@@ -203,7 +283,7 @@ end
203
283
  For help with `erb_trim_mode`, see the help documentation for `ERB::new`.
204
284
 
205
285
  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.
286
+ usually the place where they're needed most, but they work with any method.
207
287
 
208
288
  If you're seeding with a CSV, and it contains values that need to have code
209
289
  executed on them before it's imported into the database, you can define an
@@ -264,12 +344,13 @@ the `Planter::Seeder` parent class automatically provides `attr_reader :data`.
264
344
 
265
345
  Running `rails planter:seed` should now seed your `users` table.
266
346
 
267
- You can also seed children records for every existing record of a parent model.
347
+ You can also seed child records for every existing record of a parent relation.
268
348
  For example, to seed an address for every user, you'd need to create an
269
349
  `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.
350
+ is interpreted by the configured adapter. With the default Active Record adapter,
351
+ it should be the name of the `belongs_to` association on a model-backed table.
352
+ The primary key, foreign key, and persistence details will all be determined by
353
+ the adapter.
273
354
 
274
355
  ```ruby
275
356
  require 'faker'
@@ -289,11 +370,13 @@ end
289
370
  ```
290
371
 
291
372
  Note that specifying `number_of_records` in this instance will create that many
292
- records *for each record of the parent model*.
373
+ records *for each record of the parent relation*.
293
374
 
294
375
  ### Custom seeds
295
- To write your own custom seeds, just overload the `seed` method and do whatever
296
- you need to do.
376
+ To write your own custom seeds, generate with `--seeding-method=custom` or
377
+ override the `seed` method and do whatever you need to do. This is the right
378
+ choice when the seed depends on application-specific service objects, multiple
379
+ tables, or logic that does not map cleanly to CSV or `data_array`.
297
380
 
298
381
  ```ruby
299
382
  class UsersSeeder < Planter::Seeder
@@ -312,7 +395,13 @@ end
312
395
  Active Record is the default adapter, but you can provide your own adapter
313
396
  object in the initializer. Replace the generated Active Record adapter require
314
397
  and configuration with your custom adapter, while keeping `config.seeders` as
315
- your ordered seed plan.
398
+ your ordered seed plan. The default adapter creates records through Active
399
+ Record models when they exist, and falls back to direct table inserts for
400
+ model-less tables such as join tables.
401
+
402
+ For a full tutorial, see the
403
+ [Writing a Custom Adapter](https://github.com/evanthegrayt/planter/wiki/Writing-a-Custom-Adapter)
404
+ wiki page.
316
405
 
317
406
  You can generate a custom adapter stub with the following command.
318
407
 
@@ -320,11 +409,8 @@ You can generate a custom adapter stub with the following command.
320
409
  $ rails generate planter:adapter sequel
321
410
  ```
322
411
 
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`.
412
+ This creates `lib/planter/adapters/sequel.rb` with the required adapter methods,
413
+ updates `config/initializers/planter.rb`, and leaves `config.seeders` unchanged.
328
414
 
329
415
  ```ruby
330
416
  require 'planter'
@@ -335,25 +421,26 @@ Planter.configure do |config|
335
421
  end
336
422
  ```
337
423
 
338
- Custom adapters are duck typed. They should implement the same public API as
424
+ Custom adapters are duck typed. They do not need to expose model objects; at
425
+ minimum, they should implement the same table-oriented public API as
339
426
  `Planter::Adapters::ActiveRecord`.
340
427
 
341
428
  ```ruby
342
429
  class MyAdapter
343
- def create_record(model_name:, lookup_attributes:, create_attributes:)
344
- # Find or create a record for model_name.
430
+ def create_record(context:, lookup_attributes:, create_attributes:)
431
+ # Find or create a record for context.table_name.
345
432
  end
346
433
 
347
- def parent_ids(model_name:, parent:)
434
+ def parent_ids(context:)
348
435
  # Return ids for each parent record used by parent seeding.
349
436
  end
350
437
 
351
- def foreign_key(model_name:, parent:)
438
+ def foreign_key(context:)
352
439
  # Return the attribute used to attach a parent id to the seeded record.
353
440
  end
354
441
 
355
- def table_columns(model_name:)
356
- # Return native columns or fields for model_name.
442
+ def table_columns(context:)
443
+ # Return native columns or fields for context.table_name.
357
444
  end
358
445
 
359
446
  def table_names
@@ -372,8 +459,10 @@ issue](https://github.com/evanthegrayt/planter/issues/new). Just make sure
372
459
  the topic doesn't already exist. Better yet, you can always submit a Pull
373
460
  Request.
374
461
 
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)!
462
+ ## Support this project
463
+ I love knowing when people find my work useful. Any kind of support is very much
464
+ appreciated!
465
+
466
+ - ⭐️ Like the project? Star [the repository](https://github.com/evanthegrayt/planter)!
467
+ - ❤️ Love the project? Follow me [on GitHub](https://github.com/evanthegrayt)!
468
+ - 💸 *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