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.
@@ -4,30 +4,35 @@ module Planter
4
4
  ##
5
5
  # Class that seeders should inherit from. Seeders should be in +db/seeds+,
6
6
  # and named +TABLE_seeder.rb+, where +TABLE+ is the name of the table being
7
- # seeded (I.E. +users_seeder.rb+). If your seeder is named differently than
8
- # the table, you'll need to specify the table with the +model+ option. The
7
+ # seeded (for example, +users_seeder.rb+). If your seeder is named differently
8
+ # than the table, you'll need to specify the table with the +table+ option. The
9
9
  # seeder's class name should be the same as the file name, but camelized. So,
10
10
  # +UsersSeeder+. The directory where the seeder files are located can be
11
11
  # changed via an initializer.
12
12
  #
13
+ # To generate a seeder with a specific style, pass +--seeding-method=csv+,
14
+ # +--seeding-method=data-array+, or +--seeding-method=custom+ to
15
+ # +rails generate planter:seeder+. When the +csv+ method is used, the generator
16
+ # also creates a CSV file with headers pulled from the table.
17
+ #
13
18
  # The most basic way to seed is to have a CSV file with the same name as the
14
19
  # table in +db/seed_files/+. So, +users.csv+. This CSV should have the
15
- # table's column names as header. To seed using this method, your class
16
- # should look like the following. Note that +:csv_name+ and +:model+ are only
17
- # required if your seeder or csv are named differently than the table being
20
+ # table's column names as headers. To seed using this method, your class
21
+ # should look like the following. Note that +:csv_name+ and +:table+ are only
22
+ # required if your seeder or CSV are named differently than the table being
18
23
  # seeded. The directory where the seed files are kept can be changed via an
19
24
  # initializer.
20
25
  # # db/seeds/users_seeder.rb
21
26
  # require 'planter'
22
27
  # class UsersSeeder < Planter::Seeder
23
- # seeding_method :csv, csv_name: :users, model: 'User'
28
+ # seeding_method :csv, csv_name: :users, table: :users
24
29
  # end
25
30
  #
26
31
  # Another way to seed is to create records from a data array. To do this,
27
32
  # your class must implement a +data+ attribute or method, which is an array
28
33
  # of hashes. Note that this class already provides the +attr_reader+ for this
29
34
  # attribute, so the most you have to do is create instance variables in your
30
- # constructor. If if you want your data to be different for each new record
35
+ # constructor. If you want your data to be different for each new record
31
36
  # (via Faker, +Array#sample+, etc.), you'll probably want to supply a method
32
37
  # called data that returns an array of new data each time.
33
38
  # require 'planter'
@@ -39,13 +44,12 @@ module Planter
39
44
  # end
40
45
  #
41
46
  # 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
+ # is interpreted by the configured adapter. With the default Active Record
48
+ # adapter, +parent+ is the +belongs_to+ association name on a model-backed
49
+ # table. When specified, records will be created for each record in the parent
50
+ # table. Note that nothing is automatically done to prevent any validation
51
+ # errors; you must do this on your own, most likely using +Faker+ or a similar
52
+ # library.
49
53
  # require 'planter'
50
54
  # class UsersSeeder < Planter::Seeder
51
55
  # seeding_method :data_array, parent: :person
@@ -55,7 +59,7 @@ module Planter
55
59
  # end
56
60
  #
57
61
  # You can also set +number_of_records+ to determine how many times each
58
- # record in the +data+ array will get created. The default is 1. Note that if
62
+ # record in the +data+ array will be created. The default is 1. Note that if
59
63
  # this attribute is set alongside +parent+, +number_of_records+ will be how
60
64
  # many records will be created for each record in the parent table.
61
65
  # require 'planter'
@@ -68,7 +72,7 @@ module Planter
68
72
  #
69
73
  # By default, all fields are used to look up the record. If it already
70
74
  # exists, it is not re-created. If you have specific fields that a record
71
- # should be looked-up by, you can pass the +unique_columns+ option. This will
75
+ # should be looked up by, you can pass the +unique_columns+ option. This will
72
76
  # attempt to look up the record by those fields only, and if one doesn't
73
77
  # exist, one will be created with the rest of the attributes. An example of
74
78
  # when this would be useful is with Devise; you can't pass +password+ in the
@@ -97,14 +101,14 @@ module Planter
97
101
  # attribute when using +data_array+ seeding method, although it's probably
98
102
  # more likely that you'll want to define a method that returns a new set of
99
103
  # data each time (via +Faker+, +Array#sample+, etc.). When using +csv+,
100
- # +data+ will be set to the data within the csv. You can override this.
104
+ # +data+ will be set to the data within the CSV. You can override this.
101
105
  #
102
106
  # @return [Array]
103
107
  attr_reader :data
104
108
 
105
109
  ##
106
110
  # 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,
111
+ # is most useful for when seeding from CSV, and you need to transform, say,
108
112
  # 'true' (String) into true (Boolean). The user may define this as an
109
113
  # instance variable, or define a method that returns the hash.
110
114
  #
@@ -145,16 +149,15 @@ module Planter
145
149
  class_attribute :unique_columns
146
150
 
147
151
  ##
148
- # The model for the table being seeded. If the model name you need is
149
- # different, change via +seeding_method+.
152
+ # The table being seeded. If the table name you need is different, change
153
+ # via +seeding_method+.
150
154
  #
151
155
  # @return [String]
152
- class_attribute :model
156
+ class_attribute :table_name
153
157
 
154
158
  ##
155
- # The model of the parent. When provided with +association+, records in the
156
- # +data+ array, will be created for each record in the parent table. Your
157
- # class must set this attribute via +seeding_method+.
159
+ # Adapter-defined parent relation. Records in the +data+ array will be
160
+ # created for each record in the parent relation.
158
161
  #
159
162
  # @return [String]
160
163
  class_attribute :parent
@@ -168,7 +171,7 @@ module Planter
168
171
  class_attribute :number_of_records
169
172
 
170
173
  ##
171
- # The csv file corresponding to the model.
174
+ # The CSV file corresponding to the table.
172
175
  #
173
176
  # @return [String]
174
177
  class_attribute :csv_name
@@ -188,7 +191,7 @@ module Planter
188
191
  #
189
192
  # @kwarg [Integer] number_of_records
190
193
  #
191
- # @kwarg [String] model
194
+ # @kwarg [String, Symbol] table
192
195
  #
193
196
  # @kwarg [Symbol, String] parent
194
197
  #
@@ -203,7 +206,7 @@ module Planter
203
206
  # class UsersSeeder < Planter::Seeder
204
207
  # seeding_method :csv,
205
208
  # number_of_records: 2,
206
- # model: 'User'
209
+ # table: :users
207
210
  # parent: :person,
208
211
  # csv_name: :awesome_users,
209
212
  # unique_columns %i[username email],
@@ -212,7 +215,7 @@ module Planter
212
215
  def self.seeding_method(
213
216
  seed_method,
214
217
  number_of_records: 1,
215
- model: nil,
218
+ table: nil,
216
219
  parent: nil,
217
220
  csv_name: nil,
218
221
  unique_columns: nil,
@@ -224,9 +227,9 @@ module Planter
224
227
 
225
228
  self.seed_method = seed_method
226
229
  self.number_of_records = number_of_records
227
- self.model = model || to_s.delete_suffix("Seeder").singularize
230
+ self.table_name = (table || default_table_name).to_s
228
231
  self.parent = parent
229
- self.csv_name = csv_name || to_s.delete_suffix("Seeder").underscore
232
+ self.csv_name = (csv_name || table_name).to_s
230
233
  self.erb_trim_mode = erb_trim_mode || Planter.config.erb_trim_mode
231
234
  self.unique_columns =
232
235
  case unique_columns
@@ -235,14 +238,22 @@ module Planter
235
238
  end
236
239
  end
237
240
 
241
+ ##
242
+ # Return the table name inferred from the seeder class name.
243
+ #
244
+ # @return [String]
245
+ def self.default_table_name
246
+ to_s.delete_suffix("Seeder").underscore
247
+ end
248
+
238
249
  ##
239
250
  # The default seed method. To use this method, your class must provide a
240
251
  # valid +seeding_method+, and not implement its own +seed+ method.
241
252
  def seed
242
253
  validate_attributes
243
- extract_data_from_csv if seed_method == :csv
254
+ extract_data_from_csv if context.seed_method == :csv
244
255
 
245
- parent ? create_records_from_parent : create_records
256
+ context.parent ? create_records_from_parent : create_records
246
257
  end
247
258
 
248
259
  private
@@ -250,7 +261,7 @@ module Planter
250
261
  ##
251
262
  # Creates records from the +data+ attribute.
252
263
  def create_records
253
- number_of_records.times do
264
+ context.number_of_records.times do
254
265
  data.each { |record| create_record(record) }
255
266
  end
256
267
  end
@@ -258,48 +269,30 @@ module Planter
258
269
  ##
259
270
  # Create records from the +data+ attribute for each record in the +parent+.
260
271
  def create_records_from_parent
261
- adapter.parent_ids(model_name: model, parent: parent).each do |parent_id|
262
- number_of_records.times do
272
+ adapter.parent_ids(context: context).each do |parent_id|
273
+ context.number_of_records.times do
263
274
  data.each { |record| create_record(record, parent_id: parent_id) }
264
275
  end
265
276
  end
266
277
  end
267
278
 
268
279
  def create_record(record, parent_id: nil)
269
- unique, attrs = split_record(apply_transformations(record))
270
- unique = unique.merge(foreign_key => parent_id) if parent_id
271
- unique, attrs = filter_lookup_attributes(unique, attrs)
280
+ lookup_attributes, create_attributes = record_attributes.prepare(record, parent_id: parent_id)
272
281
  adapter.create_record(
273
- model_name: model,
274
- lookup_attributes: unique,
275
- create_attributes: attrs
282
+ context: context,
283
+ lookup_attributes: lookup_attributes,
284
+ create_attributes: create_attributes
276
285
  )
277
286
  end
278
287
 
279
- def full_csv_name
280
- @full_csv_name ||=
281
- %W[#{csv_name}.csv #{csv_name}.csv.erb #{csv_name}.erb.csv]
282
- .map { |f| Rails.root.join(Planter.config.csv_files_directory, f).to_s }
283
- .find { |f| ::File.file?(f) }
284
- end
285
-
286
288
  def extract_data_from_csv
287
- contents = ::File.read(full_csv_name)
288
- if full_csv_name.include?(".erb")
289
- contents = ERB.new(contents, trim_mode: erb_trim_mode).result(binding)
290
- end
291
-
292
- @data ||= ::CSV.parse(
293
- contents,
294
- headers: true,
295
- header_converters: :symbol
296
- ).map(&:to_hash)
289
+ @data ||= csv_data_source.data
297
290
  end
298
291
 
299
292
  def validate_attributes # :nodoc:
300
- case seed_method&.intern
293
+ case context.seed_method
301
294
  when :csv
302
- raise "Couldn't find csv for #{model}" unless full_csv_name
295
+ raise "Couldn't find csv for #{context.table_name}" unless csv_data_source.path
303
296
  when :data_array
304
297
  raise "data is not defined in the seeder" if public_send(:data).nil?
305
298
  else
@@ -307,74 +300,30 @@ module Planter
307
300
  end
308
301
  end
309
302
 
310
- def apply_transformations(record)
311
- return record if public_send(:transformations).nil?
312
-
313
- record.map { |field, value| map_record(field, value, record) }.to_h
314
- end
315
-
316
- def map_record(field, value, record)
317
- [
318
- field,
319
- transformations.key?(field) ? transform(field, value, record) : value
320
- ]
321
- end
322
-
323
- def transform(field, value, record)
324
- case transformations[field].arity
325
- when 0 then transformations[field].call
326
- when 1 then transformations[field].call(value)
327
- when 2 then transformations[field].call(value, record)
328
- end
329
- end
330
-
331
- def split_record(rec) # :nodoc:
332
- return [rec, {}] unless unique_columns
333
-
334
- u = unique_columns.each_with_object({}) { |c, h| h[c] = rec[c] }
335
- [u, rec.except(*unique_columns)]
303
+ def context
304
+ @context ||= Planter::SeedContext.new(
305
+ table_name: table_name,
306
+ seed_method: seed_method,
307
+ csv_name: csv_name,
308
+ parent: parent,
309
+ number_of_records: number_of_records,
310
+ unique_columns: unique_columns,
311
+ erb_trim_mode: erb_trim_mode
312
+ )
336
313
  end
337
314
 
338
- def filter_lookup_attributes(lookup_attributes, create_attributes)
339
- return [lookup_attributes, create_attributes] unless adapter.respond_to?(:table_columns)
340
-
341
- table_columns = adapter.table_columns(model_name: model).map(&:to_s)
342
- native_lookup_attributes = lookup_attributes.select do |field, _value|
343
- table_columns.include?(field.to_s)
344
- end
345
- non_column_lookup_attributes = lookup_attributes.except(*native_lookup_attributes.keys)
346
-
347
- if non_column_lookup_attributes.any?
348
- warn_non_column_lookup_attributes(non_column_lookup_attributes.keys)
349
- end
350
-
351
- if native_lookup_attributes.empty?
352
- raise "No native lookup columns found for #{model}. " \
353
- "Add a native table column to the seed data or unique_columns."
354
- end
355
-
356
- [
357
- native_lookup_attributes,
358
- non_column_lookup_attributes.merge(create_attributes)
359
- ]
315
+ def csv_data_source
316
+ @csv_data_source ||= Planter::CsvDataSource.new(context: context, seeder: self)
360
317
  end
361
318
 
362
- def warn_non_column_lookup_attributes(fields)
363
- warning_key = [model, fields.map(&:to_s).sort]
364
- @warned_non_column_lookup_attributes ||= []
365
- return if @warned_non_column_lookup_attributes.include?(warning_key)
366
-
367
- @warned_non_column_lookup_attributes << warning_key
368
- warn(
369
- "WARNING: Planter moved non-column lookup attributes for #{model} " \
370
- "into create attributes: #{warning_key.last.join(", ")}"
319
+ def record_attributes
320
+ @record_attributes ||= Planter::RecordAttributes.new(
321
+ context: context,
322
+ adapter: adapter,
323
+ transformations_provider: -> { public_send(:transformations) }
371
324
  )
372
325
  end
373
326
 
374
- def foreign_key
375
- adapter.foreign_key(model_name: model, parent: parent)
376
- end
377
-
378
327
  def adapter
379
328
  Planter.config.adapter
380
329
  end
@@ -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
@@ -9,19 +9,19 @@ module Planter
9
9
  # Major version.
10
10
  #
11
11
  # @return [Integer]
12
- MAJOR = 0
12
+ MAJOR = 1
13
13
 
14
14
  ##
15
15
  # Minor version.
16
16
  #
17
17
  # @return [Integer]
18
- MINOR = 4
18
+ MINOR = 0
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