planter 0.3.2 → 0.4.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: cbae89995fbeb940ff4ddb50b0f8a498963d84a2a7666d9d6542786c912fb455
4
- data.tar.gz: d2e4e7c56c11b5ce5439baf13df756ad506d0ac50089334e8fa2ebea39a9c757
3
+ metadata.gz: 48ca5902ca8e033fca09dedeed1c7f62efbffc3a347149787b7282030045c740
4
+ data.tar.gz: b79c752f9db2e5ab8b7067a0a7c41066e4ad731d7090f861eb38cfd5d73ef8cc
5
5
  SHA512:
6
- metadata.gz: 412a63aad94073f9140d57b9271c3900392a6f528431ce54b1a9c2303b3fa1c1553a00b1a5e2d8b0f221b9c6d328ab95720d451b2224e44a24216808522636e3
7
- data.tar.gz: 73edbbebe39d0759c053eee8f09d2b3ea0267b7533aa720b563faaddc9582be7bec451c87221404bd8b2d58f163d12f12257694cf28203c7e6b4121b3353567a
6
+ metadata.gz: 9db7c6dc7a76a834aa21a69423f4e3bf97d637eee7169c32ec75dc773dfda69a622510ba947c969691ab894d4ed9d098222653273caae06e0e00424d9a166df3
7
+ data.tar.gz: 6923ec3963d9b6af5c48bc2deb418ab2d9885814e97243501269eb0b6379376457e2dddbc23bd907d00ed75362441bd40563de5349bf71957259ca87244beb34
data/README.md CHANGED
@@ -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.3.2'
26
+ gem 'planter', '0.4.0'
27
27
  ```
28
28
 
29
29
  And then execute:
@@ -46,8 +46,15 @@ To get started, run `rails generate planter:initializer`, which will create
46
46
 
47
47
  ```ruby
48
48
  require 'planter'
49
+ require 'planter/adapters/active_record'
49
50
 
50
51
  Planter.configure do |config|
52
+ ##
53
+ # The adapter used to create records, discover parent records, and
54
+ # inspect database table names. Active Record is used by default.
55
+ # To use a custom adapter, replace this line with your own adapter.
56
+ config.adapter = Planter::Adapters::ActiveRecord.new
57
+
51
58
  ##
52
59
  # The list of seeders. These files are stored in the
53
60
  # config.seeders_directory, which can be changed below. When a new
@@ -114,7 +121,12 @@ the generator won't know where to put the new seeders.
114
121
  If you want to generate a seeder for every table currently in your database, run
115
122
  `rails generate planter:seeder ALL`.
116
123
 
117
- You then need to choose a seeding method, of which there are currently three.
124
+ Planter uses `Planter::Adapters::ActiveRecord` by default, so the built-in
125
+ seeding methods work with Active Record models without extra configuration. See
126
+ [Custom Adapters](#custom-adapters) if you want to use a different persistence
127
+ backend.
128
+
129
+ You then need to choose a seeding method, of which there are currently two.
118
130
 
119
131
  ### Seeding from CSV
120
132
  To seed from CSV, you simply need to add the following to your seeder class.
@@ -251,9 +263,9 @@ Running `rails planter:seed` should now seed your `users` table.
251
263
  You can also seed children records for every existing record of a parent model.
252
264
  For example, to seed an address for every user, you'd need to create an
253
265
  `AddressesSeeder` that uses the `parent` option, as seen below. This option
254
- should be the name of the `belongs_to` association in your model. The primary
255
- key, foreign key, and model name of the parent will all be determined by
256
- reflecting on the association.
266
+ should be the name of the `belongs_to` association in your model when using the
267
+ default Active Record adapter. The primary key, foreign key, and model name of
268
+ the parent will all be determined by the adapter.
257
269
 
258
270
  ```ruby
259
271
  require 'faker'
@@ -292,6 +304,56 @@ class UsersSeeder < Planter::Seeder
292
304
  end
293
305
  ```
294
306
 
307
+ ## Custom Adapters
308
+ Active Record is the default adapter, but you can provide your own adapter
309
+ object in the initializer. Replace the generated Active Record adapter require
310
+ and configuration with your custom adapter, while keeping `config.seeders` as
311
+ your ordered seed plan.
312
+
313
+ You can generate a custom adapter stub with the following command.
314
+
315
+ ```bash
316
+ $ rails generate planter:adapter sequel
317
+ ```
318
+
319
+ This creates `lib/planter/adapters/sequel.rb` with the required adapter methods
320
+ and updates `config/initializers/planter.rb` to use it. The generated methods
321
+ raise `NotImplementedError` until you implement them. Running the generator
322
+ replaces the currently configured adapter line, but does not change
323
+ `config.seeders`.
324
+
325
+ ```ruby
326
+ require 'planter'
327
+ require 'my_adapter'
328
+
329
+ Planter.configure do |config|
330
+ config.adapter = MyAdapter.new
331
+ end
332
+ ```
333
+
334
+ Custom adapters are duck typed. They should implement the same public API as
335
+ `Planter::Adapters::ActiveRecord`.
336
+
337
+ ```ruby
338
+ class MyAdapter
339
+ def create_record(model_name:, lookup_attributes:, create_attributes:)
340
+ # Find or create a record for model_name.
341
+ end
342
+
343
+ def parent_ids(model_name:, parent:)
344
+ # Return ids for each parent record used by parent seeding.
345
+ end
346
+
347
+ def foreign_key(model_name:, parent:)
348
+ # Return the attribute used to attach a parent id to the seeded record.
349
+ end
350
+
351
+ def table_names
352
+ # Return table or collection names used by `rails generate planter:seeder ALL`.
353
+ end
354
+ end
355
+ ```
356
+
295
357
  ## License
296
358
  The gem is available as open source under the terms of the [MIT
297
359
  License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,140 @@
1
+ module Planter
2
+ module Generators
3
+ class AdapterGenerator < Rails::Generators::Base
4
+ argument :adapter, required: true
5
+
6
+ desc "Creates an adapter file at lib/planter/adapters"
7
+
8
+ def generate_adapter
9
+ create_file adapter_path, <<~RUBY
10
+ # frozen_string_literal: true
11
+
12
+ module Planter
13
+ module Adapters
14
+ ##
15
+ # Custom adapter for Planter.
16
+ class #{adapter_class_name}
17
+ ##
18
+ # Create a record unless one already exists.
19
+ #
20
+ # @param [String] model_name the model being seeded
21
+ #
22
+ # @param [Hash] lookup_attributes attributes used to find the record
23
+ #
24
+ # @param [Hash] create_attributes additional attributes used only when
25
+ # creating a new record
26
+ #
27
+ # @return [Object]
28
+ def create_record(model_name:, lookup_attributes:, create_attributes:)
29
+ raise NotImplementedError, "\#{self.class} must implement #create_record"
30
+ end
31
+
32
+ ##
33
+ # Return the parent ids to use when seeding child records.
34
+ #
35
+ # @param [String] model_name the model being seeded
36
+ #
37
+ # @param [String, Symbol] parent the parent association name
38
+ #
39
+ # @return [Array]
40
+ def parent_ids(model_name:, parent:)
41
+ raise NotImplementedError, "\#{self.class} must implement #parent_ids"
42
+ end
43
+
44
+ ##
45
+ # Return the foreign key used to assign a parent id on a child record.
46
+ #
47
+ # @param [String] model_name the model being seeded
48
+ #
49
+ # @param [String, Symbol] parent the parent association name
50
+ #
51
+ # @return [String, Symbol]
52
+ def foreign_key(model_name:, parent:)
53
+ raise NotImplementedError, "\#{self.class} must implement #foreign_key"
54
+ end
55
+
56
+ ##
57
+ # Return table or collection names that can have seeders generated.
58
+ #
59
+ # @return [Array<String>]
60
+ def table_names
61
+ raise NotImplementedError, "\#{self.class} must implement #table_names"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ RUBY
67
+ end
68
+
69
+ def update_initializer
70
+ contents = ::File.read(initializer_full_path)
71
+ contents = replace_or_insert_adapter_require(contents)
72
+ contents = replace_or_insert_adapter_config(contents)
73
+
74
+ ::File.write(initializer_full_path, contents)
75
+ end
76
+
77
+ private
78
+
79
+ def adapter_path
80
+ "lib/planter/adapters/#{adapter_file_name}.rb"
81
+ end
82
+
83
+ def adapter_file_name
84
+ adapter.underscore
85
+ end
86
+
87
+ def adapter_class_name
88
+ adapter.camelize
89
+ end
90
+
91
+ def adapter_require_line
92
+ "require Rails.root.join('lib/planter/adapters/#{adapter_file_name}').to_s"
93
+ end
94
+
95
+ def adapter_config_line
96
+ "config.adapter = Planter::Adapters::#{adapter_class_name}.new"
97
+ end
98
+
99
+ def initializer_path
100
+ "config/initializers/planter.rb"
101
+ end
102
+
103
+ def initializer_full_path
104
+ ::File.join(destination_root, initializer_path)
105
+ end
106
+
107
+ def replace_or_insert_adapter_require(contents)
108
+ if contents.match?(adapter_require_pattern)
109
+ contents.sub(adapter_require_pattern, adapter_require_line)
110
+ else
111
+ contents.sub(planter_require_pattern) { |line| "#{line.chomp}\n#{adapter_require_line}\n" }
112
+ end
113
+ end
114
+
115
+ def replace_or_insert_adapter_config(contents)
116
+ if contents.match?(adapter_config_pattern)
117
+ contents.sub(adapter_config_pattern) { "#{$1}#{adapter_config_line}" }
118
+ else
119
+ contents.sub(configure_pattern) { |line| "#{line}\n#{$1} #{adapter_config_line}" }
120
+ end
121
+ end
122
+
123
+ def adapter_require_pattern
124
+ /^[ \t]*require ["']planter\/adapters\/[^"']+["'][ \t]*$/
125
+ end
126
+
127
+ def planter_require_pattern
128
+ /^[ \t]*require ["']planter["'][ \t]*\n?/
129
+ end
130
+
131
+ def adapter_config_pattern
132
+ /^([ \t]*)config\.adapter\s*=.*$/
133
+ end
134
+
135
+ def configure_pattern
136
+ /^([ \t]*)Planter\.configure do \|config\|[ \t]*$/
137
+ end
138
+ end
139
+ end
140
+ end
@@ -6,8 +6,15 @@ module Planter
6
6
  def create_initializer_file
7
7
  create_file "config/initializers/planter.rb", <<~EOF
8
8
  require 'planter'
9
+ require 'planter/adapters/active_record'
9
10
 
10
11
  Planter.configure do |config|
12
+ ##
13
+ # The adapter used to create records, discover parent records, and
14
+ # inspect database table names. Active Record is used by default.
15
+ # To use a custom adapter, replace this line with your own adapter.
16
+ config.adapter = Planter::Adapters::ActiveRecord.new
17
+
11
18
  ##
12
19
  # The list of seeders. These files are stored in the
13
20
  # config.seeders_directory, which can be changed below. When a new
@@ -31,9 +31,7 @@ module Planter
31
31
  end
32
32
 
33
33
  def tables
34
- @tables ||= ActiveRecord::Base.connection.tables.reject do |table|
35
- %w[ar_internal_metadata schema_migrations].include?(table)
36
- end
34
+ @tables ||= ::Planter.config.adapter.table_names
37
35
  end
38
36
  end
39
37
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Planter
4
+ module Adapters
5
+ ##
6
+ # Default adapter for seeding Active Record models.
7
+ #
8
+ # Custom adapters should implement this public API:
9
+ # - +create_record(model_name:, lookup_attributes:, create_attributes:)+
10
+ # - +parent_ids(model_name:, parent:)+
11
+ # - +foreign_key(model_name:, parent:)+
12
+ # - +table_names+
13
+ #
14
+ # +model_name+ is the configured seeder model name. +parent+ is the
15
+ # configured parent association name. Adapters are responsible for resolving
16
+ # those values into whatever persistence or reflection objects they need.
17
+ class ActiveRecord
18
+ ##
19
+ # Create a record unless one already exists.
20
+ #
21
+ # @param [String] model_name the model being seeded
22
+ #
23
+ # @param [Hash] lookup_attributes attributes used to find the record
24
+ #
25
+ # @param [Hash] create_attributes additional attributes used only when
26
+ # creating a new record
27
+ #
28
+ # @return [Object]
29
+ def create_record(model_name:, lookup_attributes:, create_attributes:)
30
+ model_name.constantize
31
+ .where(lookup_attributes)
32
+ .first_or_create!(create_attributes)
33
+ end
34
+
35
+ ##
36
+ # Return the parent ids to use when seeding child records.
37
+ #
38
+ # @param [String] model_name the model being seeded
39
+ #
40
+ # @param [String, Symbol] parent the parent association name
41
+ #
42
+ # @return [Array]
43
+ def parent_ids(model_name:, parent:)
44
+ parent_model(model_name, parent).constantize.pluck(primary_key(model_name, parent))
45
+ end
46
+
47
+ ##
48
+ # Return the foreign key used to assign a parent id on a child record.
49
+ #
50
+ # @param [String] model_name the model being seeded
51
+ #
52
+ # @param [String, Symbol] parent the parent association name
53
+ #
54
+ # @return [String, Symbol]
55
+ def foreign_key(model_name:, parent:)
56
+ association_options(model_name, parent).fetch(:foreign_key, "#{parent}_id")
57
+ end
58
+
59
+ ##
60
+ # Return application table names that can have seeders generated.
61
+ #
62
+ # @return [Array<String>]
63
+ def table_names
64
+ ::ActiveRecord::Base.connection.tables.reject do |table|
65
+ %w[ar_internal_metadata schema_migrations].include?(table)
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def association_options(model_name, parent)
72
+ model_name.constantize.reflect_on_association(parent).options
73
+ end
74
+
75
+ def primary_key(model_name, parent)
76
+ association_options(model_name, parent).fetch(:primary_key, :id)
77
+ end
78
+
79
+ def parent_model(model_name, parent)
80
+ association_options(model_name, parent).fetch(:class_name, parent.to_s.classify)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -51,6 +51,23 @@ module Planter
51
51
  # @return [String]
52
52
  attr_accessor :erb_trim_mode
53
53
 
54
+ ##
55
+ # The adapter used to create records, discover parent records, and inspect
56
+ # database table names. Custom adapters should implement the public API
57
+ # documented by +Planter::Adapters::ActiveRecord+.
58
+ #
59
+ # @return [Object]
60
+ attr_writer :adapter
61
+
62
+ ##
63
+ # Return the configured adapter, or lazily load the default Active Record
64
+ # adapter.
65
+ #
66
+ # @return [Object]
67
+ def adapter
68
+ @adapter ||= default_adapter
69
+ end
70
+
54
71
  ##
55
72
  # Create a new instance of the config.
56
73
  def initialize
@@ -58,5 +75,13 @@ module Planter
58
75
  @seeders_directory = ::File.join("db", "seeds")
59
76
  @csv_files_directory = ::File.join("db", "seed_files")
60
77
  end
78
+
79
+ private
80
+
81
+ def default_adapter
82
+ require "planter/adapters/active_record"
83
+
84
+ Planter::Adapters::ActiveRecord.new
85
+ end
61
86
  end
62
87
  end
@@ -258,7 +258,7 @@ module Planter
258
258
  ##
259
259
  # Create records from the +data+ attribute for each record in the +parent+.
260
260
  def create_records_from_parent
261
- parent_model.constantize.pluck(primary_key).each do |parent_id|
261
+ adapter.parent_ids(model_name: model, parent: parent).each do |parent_id|
262
262
  number_of_records.times do
263
263
  data.each { |record| create_record(record, parent_id: parent_id) }
264
264
  end
@@ -267,13 +267,36 @@ module Planter
267
267
 
268
268
  def create_record(record, parent_id: nil)
269
269
  unique, attrs = split_record(apply_transformations(record))
270
- model.constantize.where(
271
- unique.tap { |u| u[foreign_key] = parent_id if parent_id }
272
- ).first_or_create!(attrs)
270
+ unique = unique.merge(foreign_key => parent_id) if parent_id
271
+ adapter.create_record(
272
+ model_name: model,
273
+ lookup_attributes: unique,
274
+ create_attributes: attrs
275
+ )
276
+ end
277
+
278
+ def full_csv_name
279
+ @full_csv_name ||=
280
+ %W[#{csv_name}.csv #{csv_name}.csv.erb #{csv_name}.erb.csv]
281
+ .map { |f| Rails.root.join(Planter.config.csv_files_directory, f).to_s }
282
+ .find { |f| ::File.file?(f) }
283
+ end
284
+
285
+ def extract_data_from_csv
286
+ contents = ::File.read(full_csv_name)
287
+ if full_csv_name.include?(".erb")
288
+ contents = ERB.new(contents, trim_mode: erb_trim_mode).result(binding)
289
+ end
290
+
291
+ @data ||= ::CSV.parse(
292
+ contents,
293
+ headers: true,
294
+ header_converters: :symbol
295
+ ).map(&:to_hash)
273
296
  end
274
297
 
275
298
  def validate_attributes # :nodoc:
276
- case seed_method.intern
299
+ case seed_method&.intern
277
300
  when :csv
278
301
  raise "Couldn't find csv for #{model}" unless full_csv_name
279
302
  when :data_array
@@ -311,44 +334,12 @@ module Planter
311
334
  [u, rec.except(*unique_columns)]
312
335
  end
313
336
 
314
- def association_options
315
- @association_options ||=
316
- model.constantize.reflect_on_association(parent).options
317
- end
318
-
319
- def primary_key
320
- @primary_key ||=
321
- association_options.fetch(:primary_key, :id)
322
- end
323
-
324
337
  def foreign_key
325
- @foreign_key ||=
326
- association_options.fetch(:foreign_key, "#{parent}_id")
327
- end
328
-
329
- def parent_model
330
- @parent_model ||=
331
- association_options.fetch(:class_name, parent.to_s.classify)
332
- end
333
-
334
- def full_csv_name
335
- @full_csv_name ||=
336
- %W[#{csv_name}.csv #{csv_name}.csv.erb #{csv_name}.erb.csv]
337
- .map { |f| Rails.root.join(Planter.config.csv_files_directory, f).to_s }
338
- .find { |f| ::File.file?(f) }
338
+ adapter.foreign_key(model_name: model, parent: parent)
339
339
  end
340
340
 
341
- def extract_data_from_csv
342
- contents = ::File.read(full_csv_name)
343
- if full_csv_name.include?(".erb")
344
- contents = ERB.new(contents, trim_mode: erb_trim_mode).result(binding)
345
- end
346
-
347
- @data ||= ::CSV.parse(
348
- contents,
349
- headers: true,
350
- header_converters: :symbol
351
- ).map(&:to_hash)
341
+ def adapter
342
+ Planter.config.adapter
352
343
  end
353
344
  end
354
345
  end
@@ -15,13 +15,13 @@ module Planter
15
15
  # Minor version.
16
16
  #
17
17
  # @return [Integer]
18
- MINOR = 3
18
+ MINOR = 4
19
19
 
20
20
  ##
21
21
  # Patch version.
22
22
  #
23
23
  # @return [Integer]
24
- PATCH = 2
24
+ PATCH = 0
25
25
 
26
26
  module_function
27
27
 
data/lib/planter.rb CHANGED
@@ -69,7 +69,10 @@ module Planter
69
69
  # Planter.seed
70
70
  def seed
71
71
  seeders = ENV["SEEDERS"]&.split(",") || config.seeders&.map(&:to_s)
72
- raise "No seeders specified" if seeders.blank?
72
+ if seeders.blank?
73
+ warn "WARNING: Planter.seed was called, but no seeders were specified. Add seeders to config.seeders in config/initializers/planter.rb or set SEEDERS."
74
+ return
75
+ end
73
76
 
74
77
  seeders.each do |s|
75
78
  require Rails.root.join(config.seeders_directory, "#{s}_seeder.rb").to_s
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: planter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Gray
@@ -57,6 +57,20 @@ dependencies:
57
57
  - - "<"
58
58
  - !ruby/object:Gem::Version
59
59
  version: '2.0'
60
+ - !ruby/object:Gem::Dependency
61
+ name: simplecov
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: 0.22.0
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: 0.22.0
60
74
  - !ruby/object:Gem::Dependency
61
75
  name: standard
62
76
  requirement: !ruby/object:Gem::Requirement
@@ -82,9 +96,11 @@ files:
82
96
  - LICENSE
83
97
  - README.md
84
98
  - Rakefile
99
+ - lib/generators/planter/adapter_generator.rb
85
100
  - lib/generators/planter/initializer_generator.rb
86
101
  - lib/generators/planter/seeder_generator.rb
87
102
  - lib/planter.rb
103
+ - lib/planter/adapters/active_record.rb
88
104
  - lib/planter/config.rb
89
105
  - lib/planter/railtie.rb
90
106
  - lib/planter/seeder.rb