planter 0.5.0 → 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 +4 -4
- data/README.md +21 -4
- data/lib/planter/validator.rb +296 -0
- data/lib/planter/version.rb +2 -2
- data/lib/planter.rb +11 -0
- data/lib/tasks/planter_tasks.rake +31 -5
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b66f9b16f720b193d4dee6e0a23382b22a073982634bccbad2d52092516090eb
|
|
4
|
+
data.tar.gz: 5d66d7905379b7b148586b52b85afb2d715501543e88f0d84a88973b734d9f6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dde579bb6c055ce0c819148d1d818b39bf44ef9e75c692799dd760c7f9a96a0828d959f3204e6741fa55c41a6b9c2d8045f746142c139b5f20f1ea450ba7b259
|
|
7
|
+
data.tar.gz: 9f208c04796ae9f80ce6d46b4ec94e56da4fc2c62c36abf3198d806621bf2c7d79d9f52fd2d30ad0ee73c94206e7f0997b953ff93dbc93b83ff8064ee7ab85ed
|
data/README.md
CHANGED
|
@@ -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.
|
|
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'
|
|
24
|
+
gem 'planter'
|
|
27
25
|
```
|
|
28
26
|
|
|
29
27
|
And then execute:
|
|
@@ -97,6 +95,25 @@ 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
119
|
the table. If you don't, specify the table with the `table` option in
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Planter
|
|
4
|
+
##
|
|
5
|
+
# Performs read-only validation of the configured seed plan.
|
|
6
|
+
#
|
|
7
|
+
# The validator checks that requested seeders can be loaded, that built-in
|
|
8
|
+
# seeding methods have the files or data they need, and that the configured
|
|
9
|
+
# adapter exposes Planter's public adapter API.
|
|
10
|
+
class Validator
|
|
11
|
+
##
|
|
12
|
+
# Structured validation result containing fatal errors and non-fatal
|
|
13
|
+
# warnings.
|
|
14
|
+
class Result
|
|
15
|
+
##
|
|
16
|
+
# Fatal validation errors.
|
|
17
|
+
#
|
|
18
|
+
# @return [Array<String>]
|
|
19
|
+
attr_reader :errors
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# Non-fatal validation warnings.
|
|
23
|
+
#
|
|
24
|
+
# @return [Array<String>]
|
|
25
|
+
attr_reader :warnings
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# Create an empty validation result.
|
|
29
|
+
def initialize
|
|
30
|
+
@errors = []
|
|
31
|
+
@warnings = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Record a fatal validation error.
|
|
36
|
+
#
|
|
37
|
+
# @param message [String]
|
|
38
|
+
def add_error(message)
|
|
39
|
+
errors << message
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Record a non-fatal validation warning.
|
|
44
|
+
#
|
|
45
|
+
# @param message [String]
|
|
46
|
+
def add_warning(message)
|
|
47
|
+
warnings << message
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Whether validation completed without fatal errors.
|
|
52
|
+
#
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def success?
|
|
55
|
+
errors.empty?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# Adapter methods required for Planter's public adapter API.
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<Symbol>]
|
|
63
|
+
REQUIRED_ADAPTER_METHODS = %i[
|
|
64
|
+
create_record
|
|
65
|
+
parent_ids
|
|
66
|
+
foreign_key
|
|
67
|
+
table_columns
|
|
68
|
+
table_names
|
|
69
|
+
].freeze
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Create a validator.
|
|
73
|
+
#
|
|
74
|
+
# @param config [Planter::Config]
|
|
75
|
+
#
|
|
76
|
+
# @param seeders [Array<String>, nil] seeders to validate
|
|
77
|
+
def initialize(config: Planter.config, seeders: nil)
|
|
78
|
+
@config = config
|
|
79
|
+
@seeders = seeders || requested_seeders
|
|
80
|
+
@result = Result.new
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Validate the seed plan without creating, updating, or deleting records.
|
|
85
|
+
#
|
|
86
|
+
# @return [Planter::Validator::Result]
|
|
87
|
+
def validate
|
|
88
|
+
seeders_present = validate_seeders_present
|
|
89
|
+
validate_adapter
|
|
90
|
+
validate_seeders if seeders_present
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
attr_reader :config, :seeders, :result
|
|
97
|
+
|
|
98
|
+
def requested_seeders
|
|
99
|
+
ENV["SEEDERS"]&.split(",") || config.seeders&.map(&:to_s)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_seeders_present
|
|
103
|
+
return true if seeders.present?
|
|
104
|
+
|
|
105
|
+
result.add_error(
|
|
106
|
+
"No seeders configured. Add seeders to config.seeders in " \
|
|
107
|
+
"config/initializers/planter.rb or set SEEDERS."
|
|
108
|
+
)
|
|
109
|
+
false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_adapter
|
|
113
|
+
REQUIRED_ADAPTER_METHODS.each do |method|
|
|
114
|
+
next if adapter.respond_to?(method)
|
|
115
|
+
|
|
116
|
+
result.add_error("Configured adapter must respond to ##{method}.")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def validate_seeders
|
|
121
|
+
seeders.each { |seeder| validate_seeder(seeder) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def validate_seeder(seeder)
|
|
125
|
+
return unless require_seeder(seeder)
|
|
126
|
+
|
|
127
|
+
seeder_class = constantize_seeder(seeder)
|
|
128
|
+
return unless seeder_class
|
|
129
|
+
return unless validate_seeder_class(seeder, seeder_class)
|
|
130
|
+
return if custom_seed?(seeder_class)
|
|
131
|
+
|
|
132
|
+
validate_default_seeder(seeder, seeder_class)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def require_seeder(seeder)
|
|
136
|
+
path = seeder_path(seeder)
|
|
137
|
+
unless ::File.file?(path)
|
|
138
|
+
result.add_error("Seeder file not found for #{seeder}: #{path}.")
|
|
139
|
+
return false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
require path
|
|
143
|
+
true
|
|
144
|
+
rescue LoadError, SyntaxError, StandardError => error
|
|
145
|
+
result.add_error(
|
|
146
|
+
"Could not load seeder file for #{seeder}: #{error.class}: #{error.message}."
|
|
147
|
+
)
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def constantize_seeder(seeder)
|
|
152
|
+
"#{seeder.camelize}Seeder".constantize
|
|
153
|
+
rescue NameError
|
|
154
|
+
result.add_error("Seeder class #{seeder.camelize}Seeder could not be constantized.")
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def validate_seeder_class(seeder, seeder_class)
|
|
159
|
+
return true if seeder_class <= Planter::Seeder
|
|
160
|
+
|
|
161
|
+
result.add_error("#{seeder.camelize}Seeder must inherit from Planter::Seeder.")
|
|
162
|
+
false
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def custom_seed?(seeder_class)
|
|
166
|
+
seeder_class.instance_method(:seed).owner != Planter::Seeder
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def validate_default_seeder(seeder, seeder_class)
|
|
170
|
+
unless Planter::Seeder::SEEDING_METHODS.include?(seeder_class.seed_method&.intern)
|
|
171
|
+
result.add_error("#{seeder.camelize}Seeder must define a valid seeding_method.")
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
case seeder_class.seed_method.intern
|
|
176
|
+
when :csv
|
|
177
|
+
validate_csv_seeder(seeder, seeder_class)
|
|
178
|
+
when :data_array
|
|
179
|
+
validate_data_array_seeder(seeder, seeder_class)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def validate_csv_seeder(seeder, seeder_class)
|
|
184
|
+
seeder_instance = build_seeder(seeder, seeder_class)
|
|
185
|
+
return unless seeder_instance
|
|
186
|
+
|
|
187
|
+
context = context_for(seeder_class)
|
|
188
|
+
data_source = Planter::CsvDataSource.new(context: context, seeder: seeder_instance)
|
|
189
|
+
unless data_source.path
|
|
190
|
+
result.add_error("CSV seed file not found for #{seeder_class.table_name}.")
|
|
191
|
+
return
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
return unless adapter.respond_to?(:table_columns)
|
|
195
|
+
|
|
196
|
+
validate_csv_headers(data_source.path, context, seeder_instance)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def validate_data_array_seeder(seeder, seeder_class)
|
|
200
|
+
seeder_instance = build_seeder(seeder, seeder_class)
|
|
201
|
+
return unless seeder_instance
|
|
202
|
+
|
|
203
|
+
return unless seeder_instance.public_send(:data).nil?
|
|
204
|
+
|
|
205
|
+
result.add_error("#{seeder.camelize}Seeder must define non-nil data.")
|
|
206
|
+
rescue => error
|
|
207
|
+
result.add_error(
|
|
208
|
+
"#{seeder.camelize}Seeder data could not be read: " \
|
|
209
|
+
"#{error.class}: #{error.message}."
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def validate_csv_headers(path, context, seeder_instance)
|
|
214
|
+
headers = csv_headers(path, context, seeder_instance)
|
|
215
|
+
return if headers.nil?
|
|
216
|
+
|
|
217
|
+
table_columns = adapter.table_columns(context: context).map(&:to_s)
|
|
218
|
+
lookup_fields = csv_lookup_fields(headers, context)
|
|
219
|
+
native_lookup_fields = lookup_fields.select do |field|
|
|
220
|
+
table_columns.include?(field.to_s)
|
|
221
|
+
end
|
|
222
|
+
non_column_lookup_fields = lookup_fields - native_lookup_fields
|
|
223
|
+
|
|
224
|
+
if native_lookup_fields.empty?
|
|
225
|
+
result.add_error(
|
|
226
|
+
"No native lookup columns found for #{context.table_name}. " \
|
|
227
|
+
"Add a native table column to the seed data or unique_columns."
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
return if non_column_lookup_fields.empty?
|
|
232
|
+
|
|
233
|
+
result.add_warning(
|
|
234
|
+
"Planter will move non-column lookup attributes for #{context.table_name} " \
|
|
235
|
+
"into create attributes: #{non_column_lookup_fields.map(&:to_s).sort.join(", ")}."
|
|
236
|
+
)
|
|
237
|
+
rescue => error
|
|
238
|
+
result.add_error(
|
|
239
|
+
"CSV headers could not be read for #{context.table_name}: " \
|
|
240
|
+
"#{error.class}: #{error.message}."
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def csv_headers(path, context, seeder_instance)
|
|
245
|
+
header = ::File.foreach(path).first.to_s
|
|
246
|
+
if path.include?(".erb") && header.include?("<%")
|
|
247
|
+
header = ERB.new(header, trim_mode: context.erb_trim_mode).result(
|
|
248
|
+
seeder_instance.instance_eval { binding }
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
::CSV.parse(header, headers: true, header_converters: :symbol).headers
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def csv_lookup_fields(headers, context)
|
|
256
|
+
fields = context.unique_columns || headers
|
|
257
|
+
fields = fields.compact.map(&:to_sym)
|
|
258
|
+
|
|
259
|
+
if context.parent && adapter.respond_to?(:foreign_key)
|
|
260
|
+
fields << adapter.foreign_key(context: context).to_sym
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
fields
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def build_seeder(seeder, seeder_class)
|
|
267
|
+
seeder_class.new
|
|
268
|
+
rescue => error
|
|
269
|
+
result.add_error(
|
|
270
|
+
"#{seeder.camelize}Seeder could not be initialized: " \
|
|
271
|
+
"#{error.class}: #{error.message}."
|
|
272
|
+
)
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def context_for(seeder_class)
|
|
277
|
+
Planter::SeedContext.new(
|
|
278
|
+
table_name: seeder_class.table_name,
|
|
279
|
+
seed_method: seeder_class.seed_method,
|
|
280
|
+
csv_name: seeder_class.csv_name,
|
|
281
|
+
parent: seeder_class.parent,
|
|
282
|
+
number_of_records: seeder_class.number_of_records,
|
|
283
|
+
unique_columns: seeder_class.unique_columns,
|
|
284
|
+
erb_trim_mode: seeder_class.erb_trim_mode
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def seeder_path(seeder)
|
|
289
|
+
Rails.root.join(config.seeders_directory, "#{seeder}_seeder.rb").to_s
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def adapter
|
|
293
|
+
@adapter ||= config.adapter
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
data/lib/planter/version.rb
CHANGED
data/lib/planter.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "planter/seed_context"
|
|
|
9
9
|
require "planter/csv_data_source"
|
|
10
10
|
require "planter/record_attributes"
|
|
11
11
|
require "planter/seeder"
|
|
12
|
+
require "planter/validator"
|
|
12
13
|
|
|
13
14
|
##
|
|
14
15
|
# The main module for the plugin. It nicely wraps the +Planter::Config+ class
|
|
@@ -83,4 +84,14 @@ module Planter
|
|
|
83
84
|
"#{s.camelize}Seeder".constantize.new.seed
|
|
84
85
|
end
|
|
85
86
|
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# Validate the configured seed plan without creating records. This checks
|
|
90
|
+
# seeder files, seeder classes, built-in seeding method configuration, CSV
|
|
91
|
+
# headers where possible, and the configured adapter API.
|
|
92
|
+
#
|
|
93
|
+
# @return [Planter::Validator::Result]
|
|
94
|
+
def validate
|
|
95
|
+
Planter::Validator.new.validate
|
|
96
|
+
end
|
|
86
97
|
end
|
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
require "planter"
|
|
2
2
|
|
|
3
3
|
namespace :planter do
|
|
4
|
+
apply_directory_overrides = lambda do
|
|
5
|
+
if ENV["SEEDERS_DIRECTORY"]
|
|
6
|
+
Planter.config.seeders_directory = ENV["SEEDERS_DIRECTORY"]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
if ENV["CSV_FILES_DIRECTORY"]
|
|
10
|
+
Planter.config.csv_files_directory = ENV["CSV_FILES_DIRECTORY"]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
4
14
|
desc "Seed application. Use this to keep planter separate from db:seed"
|
|
5
15
|
task seed: :environment do
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
ENV["SEEDERS_DIRECTORY"] && config.seeders_directory = ENV["SEEDERS_DIRECTORY"]
|
|
9
|
-
ENV["CSV_FILES_DIRECTORY"] && config.csv_files_directory = ENV["CSV_FILES_DIRECTORY"]
|
|
10
|
-
end
|
|
16
|
+
# NOTE: the seed method already looks for ENV['SEEDERS']
|
|
17
|
+
apply_directory_overrides.call
|
|
11
18
|
Planter.seed
|
|
12
19
|
end
|
|
20
|
+
|
|
21
|
+
desc "Validate Planter configuration without creating records"
|
|
22
|
+
task validate: :environment do
|
|
23
|
+
apply_directory_overrides.call
|
|
24
|
+
result = Planter.validate
|
|
25
|
+
|
|
26
|
+
result.warnings.each { |warning| warn "WARNING: #{warning}" }
|
|
27
|
+
result.errors.each { |error| warn "ERROR: #{error}" }
|
|
28
|
+
|
|
29
|
+
if result.success?
|
|
30
|
+
puts(
|
|
31
|
+
result.warnings.empty? ?
|
|
32
|
+
"Planter validation passed." :
|
|
33
|
+
"Planter validation completed with warnings."
|
|
34
|
+
)
|
|
35
|
+
else
|
|
36
|
+
abort "Planter validation failed."
|
|
37
|
+
end
|
|
38
|
+
end
|
|
13
39
|
end
|
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.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Evan Gray
|
|
@@ -107,6 +107,7 @@ files:
|
|
|
107
107
|
- lib/planter/record_attributes.rb
|
|
108
108
|
- lib/planter/seed_context.rb
|
|
109
109
|
- lib/planter/seeder.rb
|
|
110
|
+
- lib/planter/validator.rb
|
|
110
111
|
- lib/planter/version.rb
|
|
111
112
|
- lib/tasks/planter_tasks.rake
|
|
112
113
|
homepage: https://github.com/evanthegrayt/planter
|