better_seeder 0.2.2.1 → 0.2.3.1

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: 58577f255f87b959dca2b8d422ebf59f8a450ac857b05e5b9b46af1cfcd4fc9e
4
- data.tar.gz: 6c4022a220f744d03209ec5ce45c6344f328e2dbe1c46b9a7e6ae9ca323133f1
3
+ metadata.gz: d5b13470c3a8af2c331ef6254593d3268c5a82654802368c4af18608ad629185
4
+ data.tar.gz: ba63f74f95445e8b29bab7e33757c9ed460786a655fb9402733b0a7372436086
5
5
  SHA512:
6
- metadata.gz: 8f322c8907aa1db395c3a231823080c50d4825f63f1d89d8dfa52784d0e686fa064b6c57cf8c6fafc830fc650741ca8853c7fb6c45fb248ef31b16c0f94de972
7
- data.tar.gz: c9d8b6d14f21db93507ac0a5b856dd832f367fe6aab97043cf7dcb6310043b37fd79ca4a6e86a6a2a2b7368c250f0ccebb0e7e797153d06a1e5687efc2a72130
6
+ metadata.gz: 8e215fd763c8348003cff4266ba1616eca6e3430e586c754b150ae2f68c97e969c23eec7ad946fd1ba7a1656feb3c507667ef45195834cd531546ea7ecba9e42
7
+ data.tar.gz: fc8b41742698a0cd0cf9b3db03a8b57bc2342b528bde096affe737a856f2a329b66063d5130b6b5a618caa1f8c87c169f60064ca4400575514ee1eca4e115feb
data/README.md CHANGED
@@ -130,7 +130,9 @@ module MyNamespace
130
130
  generate_data: true,
131
131
  count: 50,
132
132
  load_data: true,
133
- parent: nil
133
+ parents: [
134
+ { model: ::MyNamespace::MyModelParent, column: :column_id }
135
+ ]
134
136
  }
135
137
  end
136
138
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/better_seeder/generators/structure_generator.rb
2
4
  require 'fileutils'
3
5
 
@@ -5,8 +7,8 @@ module BetterSeeder
5
7
  module Builders
6
8
  class Structure
7
9
  TEMPLATE = <<~RUBY
8
- module %{module_name}
9
- class %{class_name}Structure < ::BetterSeeder::Structure::Utils
10
+ module %<module_name>s
11
+ class %<class_name>sStructure < ::BetterSeeder::Structure::Utils
10
12
  # Defines generators for each attribute.
11
13
  def self.structure
12
14
  {
@@ -23,15 +25,15 @@ module BetterSeeder
23
25
  end
24
26
  end
25
27
 
26
- # Specific seeding configuration for %{class_name}.
28
+ # Specific seeding configuration for %<class_name>s.
27
29
  def self.seed_config
28
30
  {
29
- file_name: '%{file_name}',
31
+ file_name: '%<file_name>s',
30
32
  columns: { excluded: [] },
31
33
  generate_data: true,
32
34
  count: 10,
33
35
  load_data: true,
34
- parent: nil
36
+ # parents: [ { model: 'MyNamespace::MyModel', column: :my_column } ]
35
37
  }
36
38
  end
37
39
 
@@ -49,26 +51,23 @@ module BetterSeeder
49
51
  # @return [String] The full path to the generated file.
50
52
  def self.generate(model_name)
51
53
  # Split the model name into module parts and the actual class name.
52
- parts = model_name.split("::")
53
- class_name = parts.pop
54
- module_name = parts.empty? ? "Main" : parts.join("::")
54
+ parts = model_name.split('::')
55
+ class_name = parts.pop
56
+ module_name = parts.empty? ? 'Main' : parts.join('::')
55
57
 
56
58
  # Determine the file path.
57
59
  # For example, for "MyNamespace::MyModel", the file will be placed in:
58
60
  # lib/better_seeder/generators/my_namespace/my_model_structure.rb
59
61
  folder_path = File.join(BetterSeeder.configuration.structure_path, *parts.map(&:underscore))
60
- file_name = "#{class_name.underscore}_structure.rb"
61
- full_path = File.join(folder_path, file_name)
62
+ file_name = "#{class_name.underscore}_structure.rb"
63
+ full_path = File.join(folder_path, file_name)
62
64
 
63
65
  # Ensure the target directory exists.
64
- FileUtils.mkdir_p(folder_path) unless Dir.exist?(folder_path)
66
+ FileUtils.mkdir_p(folder_path)
65
67
 
66
68
  # Prepare the file content.
67
- content = TEMPLATE % {
68
- module_name: module_name,
69
- class_name: class_name,
70
- file_name: "#{class_name.underscore}_seed"
71
- }
69
+ content = format(TEMPLATE, module_name: module_name, class_name: class_name,
70
+ file_name: "#{class_name.underscore}_seed")
72
71
 
73
72
  # Write the template to the file.
74
73
  File.write(full_path, content)
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/better_seeder/configuration.rb
2
4
 
3
5
  module BetterSeeder
4
6
  class Configuration
5
- attr_accessor :log_language, :structure_path, :preload_path
7
+ attr_accessor :log_language, :log_level, :structure_path, :preload_path
6
8
 
7
9
  def initialize
10
+ @log_language = :en
11
+ @log_level = :info
8
12
  if defined?(Rails) && Rails.respond_to?(:root)
9
- @log_language = :en
10
13
  @structure_path = Rails.root.join('db', 'seed', 'structure')
11
14
  @preload_path = Rails.root.join('db', 'seed', 'preload')
12
15
  else
13
- @log_language = :en
14
16
  @structure_path = File.join(Dir.pwd, 'db', 'seed', 'structure')
15
17
  @preload_path = File.join(Dir.pwd, 'db', 'seed', 'preload')
16
18
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/better_seeder/exporter/base_exporter.rb
2
4
 
3
5
  module BetterSeeder
@@ -23,10 +25,10 @@ module BetterSeeder
23
25
  # @param output_path [String] Nome del file (senza estensione).
24
26
  # @param table_name [String] Nome della tabella (usato in SqlExporter).
25
27
  def initialize(data, output_path:, table_name: 'my_table')
26
- @data = data
28
+ @data = data
27
29
  # Utilizza il preload_path definito nella configurazione BetterSeeder (impostato nell'initializer).
28
30
  @output_path = File.join(BetterSeeder.configuration.preload_path, output_path)
29
- @table_name = table_name
31
+ @table_name = table_name
30
32
  end
31
33
 
32
34
  # Restituisce la directory in cui salvare i file.
@@ -39,7 +41,7 @@ module BetterSeeder
39
41
 
40
42
  # Verifica che la directory di output esista; se non esiste, la crea.
41
43
  def ensure_output_directory
42
- FileUtils.mkdir_p(output_directory) unless Dir.exist?(output_directory)
44
+ FileUtils.mkdir_p(output_directory)
43
45
  end
44
46
 
45
47
  # Costruisce il percorso completo del file di output, combinando la directory, l'output_path e l'estensione.
@@ -53,12 +55,12 @@ module BetterSeeder
53
55
  # Metodo astratto per ottenere l'estensione del file (es. ".json", ".csv", ".sql").
54
56
  # Le classi derivate devono implementarlo.
55
57
  def extension
56
- raise NotImplementedError, "Subclasses must implement #extension"
58
+ raise NotImplementedError, 'Subclasses must implement #extension'
57
59
  end
58
60
 
59
61
  # Metodo astratto per effettuare l'export.
60
62
  def export
61
- raise NotImplementedError, "Subclasses must implement the export method"
63
+ raise NotImplementedError, 'Subclasses must implement the export method'
62
64
  end
63
65
  end
64
66
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BetterSeeder
2
4
  module Exporters
3
5
  class Csv < Base
@@ -5,6 +7,7 @@ module BetterSeeder
5
7
  # Se la cartella non esiste, viene creata automaticamente.
6
8
  def export
7
9
  return if data.empty?
10
+
8
11
  headers = data.first.keys
9
12
 
10
13
  # Costruisce il percorso completo del file di output
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BetterSeeder
2
4
  module Exporters
3
5
  class Json < Base
@@ -7,9 +9,7 @@ module BetterSeeder
7
9
  # Imposta la directory di output
8
10
  full_path = File.join(full_output_path)
9
11
 
10
- File.open(full_path, 'w') do |file|
11
- file.write(JSON.pretty_generate(data))
12
- end
12
+ File.write(full_path, JSON.pretty_generate(data))
13
13
  end
14
14
 
15
15
  def extension
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BetterSeeder
2
4
  module Exporters
3
5
  class Sql < Base
@@ -11,6 +13,7 @@ module BetterSeeder
11
13
  # ... ;
12
14
  def export
13
15
  return if data.empty?
16
+
14
17
  columns = data.first.keys
15
18
 
16
19
  # Crea l'array delle tuple di valori per ciascun record.
@@ -18,7 +21,7 @@ module BetterSeeder
18
21
  row_values = columns.map do |col|
19
22
  value = row[col]
20
23
  # Se il valore è nil restituisce NULL, altrimenti esegue l'escaping delle virgolette singole.
21
- value.nil? ? "NULL" : "'#{value.to_s.gsub("'", "''")}'"
24
+ value.nil? ? 'NULL' : "'#{value.to_s.gsub("'", "''")}'"
22
25
  end
23
26
  "(#{row_values.join(', ')})"
24
27
  end
@@ -1,116 +1,132 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BetterSeeder
2
4
  module Farms
3
5
  class Farmer
4
- # Genera dati fittizi per il modello specificato utilizzando il file di structure.
5
- #
6
- # Opzioni attese (Hash):
7
- # :model => Nome del modello come stringa, es. 'Media::Participant'
8
- # :count => Numero di record da generare (default: 10)
9
- #
10
- # Se la classe di structure definisce il metodo `unique_keys` (che deve restituire un array di array,
11
- # es. [[:media_id, :creator_id], [:role]]), verrà controllato che ogni record generato sia univoco,
12
- # sia in memoria (tra quelli generati in questa esecuzione) che rispetto ai dati già presenti nel database.
13
- # Se il record è duplicato, verrà rigenerato.
14
- #
15
- # Se la classe di structure definisce il metodo `seed_schema`, il record verrà validato tramite Dry-schema.
16
- #
17
- # @return [Array<Hash>] Array di record validati e univoci generati
18
- def self.generate(options = {})
19
- model_name = options[:model] or raise ArgumentError, "Missing :model option"
20
- count = options[:count] || 10
21
-
22
- # Costruisce il percorso del file di structure.
23
- # Ad esempio, per il modello "Media::Participant", il file atteso sarà:
24
- # "db/seed/structure/media/participant_structure.rb"
25
- structure_file = File.expand_path(
26
- File.join(BetterSeeder.configuration.structure_path, "#{model_name.underscore}_structure.rb"),
27
- Dir.pwd
28
- )
29
- raise "Structure file not found: #{structure_file}" unless File.exist?(structure_file)
30
-
31
- # Carica il file di structure.
32
- load structure_file
33
-
34
- # Costruisce il nome della classe di structure: semplice concatenazione.
35
- # Es: "Media::Participant" => "Media::ParticipantStructure"
36
- structure_class_name = "#{model_name}Structure"
37
- begin
38
- structure_class = Object.const_get(structure_class_name)
39
- rescue error
40
- message = "Structure class not found: #{structure_class_name}"
41
- BetterSeeder::Utils.logger(message: message)
42
- raise error
6
+ class << self
7
+ def generate(options = {})
8
+ model_name = options[:model] or raise ArgumentError, 'Missing :model option'
9
+ count = options[:count] || 10
10
+
11
+ # Costruisce il percorso del file di structure.
12
+ structure_file = File.expand_path(
13
+ File.join(BetterSeeder.configuration.structure_path, "#{model_name.underscore}_structure.rb"),
14
+ Dir.pwd
15
+ )
16
+ raise "Structure file not found: #{structure_file}" unless File.exist?(structure_file)
17
+
18
+ # Carica il file di structure.
19
+ load structure_file
20
+
21
+ # Costruisce il nome della classe di structure: es. "Media::Participant" => "Media::ParticipantStructure"
22
+ structure_class_name = "#{model_name}Structure"
23
+ begin
24
+ structure_class = Object.const_get(structure_class_name)
25
+ rescue error
26
+ message = "Structure class not found: #{structure_class_name}"
27
+ BetterSeeder::Utils.logger(message: message)
28
+ raise error
29
+ end
30
+
31
+ generated_records = []
32
+
33
+ while generated_records.size < count
34
+ new_record = nil
35
+ loop do
36
+ new_record = build_record(model_name, structure_class)
37
+ new_record = inject_parent_keys(model_name, new_record, structure_class)
38
+ break if validate_record(new_record, structure_class) &&
39
+ !record_exists?(model_name, new_record, structure_class, generated_records)
40
+ end
41
+ generated_records.push(new_record)
42
+ end
43
+
44
+ generated_records
43
45
  end
44
46
 
45
- generation_rules = structure_class.structure
46
- raise "Structure must be a Hash" unless generation_rules.is_a?(Hash)
47
-
48
- # Recupera lo schema per la validazione, se definito.
49
- schema = structure_class.respond_to?(:seed_schema) ? structure_class.seed_schema : nil
50
-
51
- # Gestione dei vincoli di unicità.
52
- # Se il metodo unique_keys è definito, lo si aspetta come array di array,
53
- # ad esempio: [[:media_id, :creator_id], [:role]]
54
- unique_key_sets = structure_class.respond_to?(:unique_keys) ? structure_class.unique_keys : []
55
- # Pre-carica i valori già presenti nel database per ciascun gruppo di colonne.
56
- unique_sets = unique_key_sets.map do |cols|
57
- existing_keys = Set.new
58
- # Usa pluck per recuperare le colonne specificate dal modello.
59
- # Se il gruppo è di una sola colonna, pluck restituirà un array di valori; se multi, un array di array.
60
- db_rows = Object.const_get(model_name).pluck(*cols)
61
- db_rows.each do |row|
62
- composite_key = cols.size == 1 ? row.to_s : row.join("_")
63
- existing_keys.add(composite_key)
47
+ private
48
+
49
+ def record_exists?(_model_name, record, structure_class, generated_records)
50
+ # Se non è definito il metodo unique_keys, non eseguiamo il controllo
51
+ return false unless structure_class.respond_to?(:unique_keys)
52
+
53
+ unique_key_sets = structure_class.unique_keys
54
+ return false if unique_key_sets.empty?
55
+
56
+ # Determina il modello associato: si assume che il nome del modello sia
57
+ # dato dalla rimozione della stringa "Structure" dal nome della classe di structure.
58
+ model_class_name = structure_class.to_s.sub(/Structure$/, '')
59
+ model_class = Object.const_get(model_class_name)
60
+
61
+ # Per ogni set di chiavi uniche, costruiamo le condizioni della query
62
+ unique_key_sets.each do |key_set|
63
+ conditions = {}
64
+ key_set.each do |col|
65
+ conditions[col] = record[col]
66
+ end
67
+ # Se esiste un record nel database che soddisfa le condizioni, restituisce true.
68
+ return true if generated_records.find do |record|
69
+ conditions.all? { |key, value| record[key] == value }
70
+ end.present?
71
+ return true if model_class.where(conditions).exists?
64
72
  end
65
- { columns: cols, set: existing_keys }
73
+
74
+ false
66
75
  end
67
76
 
68
- generated_records = []
69
- progressbar = ProgressBar.create(total: count, format: '%a %B %p%% %t')
70
- attempts = 0
77
+ def build_record(_model_name, structure_class)
78
+ generation_rules = structure_class.structure
79
+ raise 'Structure must be a Hash' unless generation_rules.is_a?(Hash)
71
80
 
72
- # Continua a generare record finché non si raggiunge il numero richiesto.
73
- while generated_records.size < count
74
- attempts += 1
75
81
  record = {}
76
82
  generation_rules.each do |attribute, rule|
77
- # Ogni rule è un array: [tipo, generatore]
78
- generator = rule[1]
79
- value = generator.respond_to?(:call) ? generator.call : generator
83
+ # Ogni rule è un array nel formato [tipo, generatore]
84
+ generator = rule[1]
85
+ value = generator.respond_to?(:call) ? generator.call : generator
80
86
  record[attribute] = value
81
87
  end
82
88
 
83
- # Se è definito uno schema, valida il record.
84
- if schema
85
- result = schema.call(record)
86
- unless result.success?
87
- message = "[ERROR] Record validation failed for #{model_name}: #{result.errors.to_h}"
88
- BetterSeeder::Utils.logger(message: message)
89
- progressbar.increment
90
- next # Rigenera il record se la validazione fallisce.
89
+ record
90
+ end
91
+
92
+ def inject_parent_keys(_model_name, record, structure_class)
93
+ config = structure_class.respond_to?(:seed_config) ? structure_class.seed_config : {}
94
+ parents_spec = config[:parents]
95
+ return record if parents_spec.blank?
96
+
97
+ parents_spec.each do |parent_config|
98
+ parent_model = parent_config[:model]
99
+ column = parent_config[:column]
100
+
101
+ # Tenta di ottenere un record del parent dal pool BetterSeeder.generated_records se disponibile.
102
+ # Usiamo il nome del modello come chiave nel pool.
103
+ pool_key = parent_model.to_s
104
+ unless defined?(BetterSeeder.generated_records) &&
105
+ BetterSeeder.generated_records[pool_key] &&
106
+ !BetterSeeder.generated_records[pool_key].empty?
107
+ BetterSeeder.generated_records[pool_key] = parent_model.all
91
108
  end
92
- end
109
+ parent_record = BetterSeeder.generated_records[pool_key].sample
93
110
 
94
- # Controlla i vincoli di unicità: verifica che il record non sia già presente
95
- duplicate = unique_sets.any? do |unique_set|
96
- composite_key = unique_set[:columns].map { |col| record[col].to_s }.join("_")
97
- unique_set[:set].include?(composite_key)
98
- end
99
- next if duplicate
111
+ raise "Parent record not found for #{parent_model}" unless parent_record
100
112
 
101
- # Aggiorna le strutture per il controllo di unicità con il nuovo record.
102
- unique_sets.each do |unique_set|
103
- composite_key = unique_set[:columns].map { |col| record[col].to_s }.join("_")
104
- unique_set[:set].add(composite_key)
113
+ # Inietta nel record la chiave esterna indicata nella configurazione.
114
+ # binding.pry if model_name == "Media::Participant"
115
+ record[column] = parent_record[:id]
105
116
  end
106
117
 
107
- generated_records << record
108
- progressbar.increment
118
+ record
109
119
  end
110
120
 
111
- message = "[INFO] Generated #{generated_records.size} unique records for #{model_name} after #{attempts} attempts."
112
- BetterSeeder::Utils.logger(message: message)
113
- generated_records
121
+ def validate_record(record, structure_class)
122
+ return true unless structure_class.respond_to?(:seed_schema_validation)
123
+
124
+ schema = structure_class.seed_schema_validation
125
+ result = schema.call(record)
126
+ return true if result.success?
127
+
128
+ raise result.errors.to_h
129
+ end
114
130
  end
115
131
  end
116
132
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry-types'
2
4
 
3
5
  module BetterSeeder
@@ -1,21 +1,74 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/better_seeder/utils.rb
4
+ #
5
+ # = BetterSeeder::Utils
6
+ #
7
+ # Questo modulo fornisce metodi di utilità per la gestione dei seed. In particolare,
8
+ # consente di trasformare i nomi delle classi in formato snake_case con il suffisso "_structure.rb",
9
+ # gestire i messaggi di log e configurare il livello del logger per ActiveRecord.
2
10
 
3
11
  module BetterSeeder
4
12
  module Utils
5
- # Trasforma un nome di classe in snake_case.
6
- # Esempio: "Campaigns::Campaign" => "campaigns_campaign"
7
- def self.transform_class_name(class_name)
8
- elements = class_name.split("::").map(&:underscore)
9
- # Aggiunge "_structure.rb" all'ultimo elemento
10
- elements[-1] = "#{elements[-1]}_structure.rb"
11
- elements.join("/")
12
- end
13
+ class << self
14
+ ##
15
+ # Trasforma un nome di classe in snake_case e aggiunge il suffisso "_structure.rb".
16
+ #
17
+ # ==== Esempio
18
+ # transform_class_name("Campaigns::Campaign")
19
+ # # => "campaigns/campaign_structure.rb"
20
+ #
21
+ # ==== Parametri
22
+ # * +class_name+ - Stringa che rappresenta il nome della classe, eventualmente suddiviso in
23
+ # namespace separati da "::".
24
+ #
25
+ # ==== Ritorno
26
+ # Restituisce una stringa con il nome della classe in formato snake_case e l'ultimo elemento
27
+ # terminato con "_structure.rb".
28
+ #
29
+ def transform_class_name(class_name)
30
+ elements = class_name.split('::').map(&:underscore)
31
+ # Aggiunge "_structure.rb" all'ultimo elemento
32
+ elements[-1] = "#{elements[-1]}_structure.rb"
33
+ elements.join('/')
34
+ end
35
+
36
+ ##
37
+ # Registra un messaggio usando il logger di Rails se disponibile, altrimenti lo stampa su standard output.
38
+ #
39
+ # ==== Parametri
40
+ # * +message+ - Il messaggio da loggare (può essere una stringa o nil).
41
+ #
42
+ # ==== Ritorno
43
+ # Non ritorna un valore significativo.
44
+ #
45
+ def logger(message: nil)
46
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
47
+ Rails.logger.info message
48
+ else
49
+ puts message
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Configura il livello del logger per ActiveRecord in base alla configurazione definita in BetterSeeder.
55
+ #
56
+ # ==== Dettagli
57
+ # Il metodo imposta il livello del logger in base al valore di BetterSeeder.configuration.log_level:
58
+ # * +:debug+ -> Logger::DEBUG
59
+ # * +:info+ -> Logger::INFO
60
+ # * +:error+ -> Logger::ERROR
61
+ # Se il livello non corrisponde a nessuna delle opzioni previste, viene impostato il livello +Logger::DEBUG+.
62
+ #
63
+ def log_level_setup
64
+ level = case BetterSeeder.configuration.log_level
65
+ when :debug then Logger::DEBUG
66
+ when :info then Logger::INFO
67
+ when :error then Logger::ERROR
68
+ else Logger::DEBUG
69
+ end
13
70
 
14
- def self.logger(message: nil)
15
- if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
16
- Rails.logger.info message
17
- else
18
- puts message
71
+ ActiveRecord::Base.logger.level = level
19
72
  end
20
73
  end
21
74
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BetterSeeder
4
- VERSION = "0.2.2.1"
4
+ VERSION = '0.2.3.1'
5
5
  end
data/lib/better_seeder.rb CHANGED
@@ -1,30 +1,44 @@
1
- require_relative "better_seeder/utils"
2
- require_relative "better_seeder/configuration"
3
- require_relative "better_seeder/structure/utils"
4
- require_relative "better_seeder/farms/farmer"
5
- require_relative "better_seeder/exporters/base"
6
- require_relative "better_seeder/exporters/json"
7
- require_relative "better_seeder/exporters/csv"
8
- require_relative "better_seeder/exporters/sql"
9
- require_relative "better_seeder/builders/structure"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'better_seeder/utils'
4
+ require_relative 'better_seeder/configuration'
5
+ require_relative 'better_seeder/structure/utils'
6
+ require_relative 'better_seeder/farms/farmer'
7
+ require_relative 'better_seeder/exporters/base'
8
+ require_relative 'better_seeder/exporters/json'
9
+ require_relative 'better_seeder/exporters/csv'
10
+ require_relative 'better_seeder/exporters/sql'
11
+ require_relative 'better_seeder/builders/structure'
10
12
 
11
13
  module BetterSeeder
12
14
  class Configuration
13
15
  attr_accessor :log_language, :structure_path, :preload_path
14
16
 
15
17
  def initialize
18
+ @log_language = :en
19
+ @log_level = :info
16
20
  if defined?(Rails) && Rails.respond_to?(:root)
17
- @log_language = :en
18
21
  @structure_path = Rails.root.join('db', 'seed', 'structure')
19
22
  @preload_path = Rails.root.join('db', 'seed', 'preload')
20
23
  else
21
- @log_language = :en
22
24
  @structure_path = File.join(Dir.pwd, 'db', 'seed', 'structure')
23
25
  @preload_path = File.join(Dir.pwd, 'db', 'seed', 'preload')
24
26
  end
25
27
  end
26
28
  end
27
29
 
30
+ # Definisce un hash che fungerà da pool per memorizzare i record generati per ciascun modello.
31
+ @generated_records = {}
32
+
33
+ def self.generated_records
34
+ @generated_records
35
+ end
36
+
37
+ def self.store_generated_record(model_name, record)
38
+ @generated_records[model_name.to_s] ||= []
39
+ @generated_records[model_name.to_s] << record
40
+ end
41
+
28
42
  # Restituisce l'istanza globale di configurazione. Se non esiste, viene creata con i valori di default.
29
43
  def self.configuration
30
44
  @configuration ||= Configuration.new
@@ -47,10 +61,11 @@ module BetterSeeder
47
61
  # Metodo install che crea l'initializer di BetterSeeder in config/initializers
48
62
  # con le seguenti impostazioni:
49
63
  # - log_language: lingua da usare per i log (es. :en, :it)
64
+ # - log_level: livello di logging nell'output dei dati del seeder (:debug, :info, :error)
50
65
  # - structure_path: percorso dove sono memorizzati i file di structure (default: Rails.root/db/seed/structure)
51
66
  # - preload_path: percorso dove verranno salvati i file esportati (default: Rails.root/db/seed/preload)
52
67
  def self.install
53
- initializer_path = File.join(Rails.root, "config", "initializers", "better_seeder.rb")
68
+ initializer_path = Rails.root.join('config', 'initializers', 'better_seeder.rb').to_s
54
69
 
55
70
  if File.exist?(initializer_path)
56
71
  message = "BetterSeeder initializer already exists at #{initializer_path}"
@@ -60,6 +75,7 @@ module BetterSeeder
60
75
  # This file was generated by BetterSeeder.install
61
76
  BetterSeeder.configure do |config|
62
77
  config.log_language = :en
78
+ config.log_level = :error
63
79
  config.structure_path = Rails.root.join('db', 'seed', 'structure')
64
80
  config.preload_path = Rails.root.join('db', 'seed', 'preload')
65
81
  end
@@ -94,9 +110,15 @@ module BetterSeeder
94
110
  # - Se abilitato, i record vengono caricati nel database e successivamente esportati nel formato richiesto
95
111
  # - Vengono raccolte statistiche e loggato il tempo totale di esecuzione
96
112
  def self.magic(config)
97
- start_time = Time.now
98
- stats = {} # Statistiche: modello => numero di record caricati
99
- parent_loaded_records = {} # Per memorizzare i record creati per i modelli parent
113
+ previous_log_level = ActiveRecord::Base.logger.level
114
+ BetterSeeder::Utils.log_level_setup
115
+
116
+ message = "[LOGGER] previous log level: #{previous_log_level}, actual log level: #{ActiveRecord::Base.logger.level}"
117
+ BetterSeeder::Utils.logger(message: message)
118
+
119
+ start_time = Time.zone.now
120
+ stats = {} # Statistiche: modello => numero di record caricati
121
+ parent_loaded_records = {} # Per memorizzare i record creati per i modelli parent
100
122
 
101
123
  ActiveRecord::Base.transaction do
102
124
  export_type = config[:configurations][:export_type]
@@ -106,16 +128,15 @@ module BetterSeeder
106
128
  end
107
129
  end
108
130
 
109
- total_time = Time.now - start_time
131
+ ActiveRecord::Base.logger.level = previous_log_level
132
+ total_time = Time.zone.now - start_time
110
133
  log_statistics(stats, total_time)
111
134
  end
112
135
 
113
- def self.generate_structure(model_name: )
136
+ def self.generate_structure(model_name:)
114
137
  BetterSeeder::Builders::Structure.generate(model_name)
115
138
  end
116
139
 
117
- private
118
-
119
140
  # Processa la configurazione per un singolo modello.
120
141
  # Carica il file di structure corrispondente e recupera la configurazione tramite `seed_config`.
121
142
  # Quindi, esegue il recupero o la generazione dei record, iniezione di eventuali foreign key per modelli child,
@@ -145,13 +166,18 @@ module BetterSeeder
145
166
 
146
167
  # Recupera la configurazione specifica dal file di structure tramite il metodo seed_config.
147
168
  # Se non definito, vengono usati dei valori di default.
148
- seed_config = structure_class.respond_to?(:seed_config) ? structure_class.seed_config : {}
169
+ seed_config = structure_class.respond_to?(:seed_config) ? structure_class.seed_config : {}
149
170
  file_name = seed_config[:file_name] || "#{model_name.underscore}_seed"
150
- excluded_columns = seed_config.dig(:columns, :excluded) || []
171
+ excluded_columns = if export_type.to_s.downcase == 'sql'
172
+ []
173
+ else
174
+ seed_config.dig(:columns, :excluded) || []
175
+ end
151
176
  generate_data = seed_config.fetch(:generate_data, true)
152
177
  count = seed_config[:count] || 10
153
178
  load_data = seed_config.fetch(:load_data, true)
154
- parent = seed_config[:parent] # nil oppure valore (o array) per modelli child
179
+ parent = seed_config[:parent] # nil oppure valore (o array) per modelli child
180
+ superclass = seed_config[:superclass] # nil oppure valore (o array) per modelli child
155
181
 
156
182
  # Log per indicare se il modello è parent o child.
157
183
  message = if parent.nil?
@@ -162,7 +188,11 @@ module BetterSeeder
162
188
  BetterSeeder::Utils.logger(message: message)
163
189
 
164
190
  # Recupera la classe reale del modello (ActiveRecord).
165
- model_class = Object.const_get(model_name) rescue nil
191
+ model_class = begin
192
+ Object.const_get(model_name)
193
+ rescue StandardError
194
+ nil
195
+ end
166
196
  unless model_class
167
197
  message = "[ERROR] Model #{model_name} not found."
168
198
  BetterSeeder::Utils.logger(message: message)
@@ -170,45 +200,28 @@ module BetterSeeder
170
200
  raise Object.const_get(model_name)
171
201
  end
172
202
 
173
- # Recupera o genera i record.
174
- records = if generate_data
175
- Farms::Farmer.generate(model: model_name, count: count)
176
- else
177
- model_class.all.map(&:attributes)
178
- end
179
-
180
- # Rimuove le colonne escluse.
181
- processed_records = records.map do |record|
182
- record.reject { |key, _| excluded_columns.include?(key.to_sym) }
183
- end
184
-
185
- # Se il modello è child, inietta le foreign key.
186
- if parent
187
- Array(parent).each do |parent_model_name|
188
- parent_records = parent_loaded_records[parent_model_name]
189
- if parent_records.nil? || parent_records.empty?
190
- message = "[ERROR] No loaded records found for parent model #{parent_model_name}. Cannot assign foreign key for #{model_name}."
191
- BetterSeeder::Utils.logger(message: message)
192
- else
193
- # Il nome della foreign key è ottenuto prendendo l'ultima parte del nome del modello padre,
194
- # trasformandola in minuscolo e in forma singolare, e aggiungendo "_id".
195
- foreign_key = parent_model_name.split("::").last.underscore.singularize + "_id"
196
- processed_records.each do |record|
197
- record[foreign_key] = parent_records.sample.id
198
- end
199
- end
200
- end
201
- end
202
-
203
203
  # Se abilitato, carica i record nel database.
204
- if load_data
205
- total_records = processed_records.size
206
- stats[model_name] = total_records
207
- created_records = load_records_into_db(model_class, processed_records, total_records, model_name)
204
+ if load_data && File.exist?("#{BetterSeeder.configuration.preload_path}/#{seed_config[:file_name]}.sql")
205
+ load_data_from_file(seed_config)
206
+ stats[model_name] = model_class.count
207
+ elsif generate_data
208
+ records = Farms::Farmer.generate(model: model_name, count: count)
209
+ total_records = records.size
210
+ stats[model_name] = total_records
211
+ created_records = load_records_into_db(model_class, records, total_records, model_name,
212
+ superclass)
208
213
  # Se il modello è parent, salva i record creati per poterli utilizzare in seguito per i modelli child.
209
214
  parent_loaded_records[model_name] = created_records if parent.nil?
210
215
  else
211
- stats[model_name] = 0
216
+ model_class.all.map(&:attributes)
217
+ end
218
+
219
+ # Rimuove le colonne escluse.
220
+ return if BetterSeeder.generated_records[(superclass || model_name).to_s].nil?
221
+
222
+ processed_records = BetterSeeder.generated_records[(superclass || model_name).to_s]
223
+ processed_records = processed_records.map do |campaign|
224
+ campaign.attributes.except(*excluded_columns.map(&:to_s))
212
225
  end
213
226
 
214
227
  # Esporta i record nel formato richiesto.
@@ -219,25 +232,21 @@ module BetterSeeder
219
232
  # I log delle query SQL vengono temporaneamente disabilitati.
220
233
  #
221
234
  # @return [Array<Object>] Array dei record creati (istanze ActiveRecord)
222
- def self.load_records_into_db(model_class, processed_records, total_records, model_name)
223
- created_records = []
235
+ def self.load_records_into_db(model_class, processed_records, total_records, model_name, superclass)
224
236
  progressbar = ProgressBar.create(total: total_records, format: '%a %B %p%% %t')
225
- message = "[INFO] Starting to load #{total_records} records for model #{model_name}..."
237
+ message = "[INFO] Starting to load #{total_records} records for model #{model_name}..."
226
238
  BetterSeeder::Utils.logger(message: message)
227
239
 
228
- previous_level = ActiveRecord::Base.logger.level
229
- ActiveRecord::Base.logger.level = Logger::ERROR
230
-
231
240
  processed_records.each do |record|
232
241
  created = model_class.create!(record)
233
- created_records << created
242
+ BetterSeeder.store_generated_record(superclass || model_name, created)
234
243
  progressbar.increment
235
244
  end
236
245
 
237
- ActiveRecord::Base.logger.level = previous_level
238
246
  message = "[INFO] Finished loading #{total_records} records into model #{model_name}."
239
247
  BetterSeeder::Utils.logger(message: message)
240
- created_records
248
+
249
+ BetterSeeder.generated_records[model_name]
241
250
  end
242
251
 
243
252
  # Esporta i record nel formato specificato (json, csv, sql).
@@ -261,14 +270,39 @@ module BetterSeeder
261
270
 
262
271
  # Log finale con le statistiche raccolte e il tempo totale di esecuzione.
263
272
  def self.log_statistics(stats, total_time)
264
- stats_message = stats.map { |model, count| "#{model}: #{count} records" }.join(", ")
265
- message = "[INFO] Finished processing all models in #{total_time.round(2)} seconds. Statistics: #{stats_message}"
273
+ stats_message = stats.map { |model, count| "#{model}: #{count} records" }.join("\n")
274
+ message = "[INFO] Finished processing all models in #{total_time.round(2)} seconds. Statistics: \n#{stats_message}"
266
275
  BetterSeeder::Utils.logger(message: message)
267
276
  end
268
277
 
269
278
  # Metodo di utilità per trasformare il nome della classe in un formato in cui le lettere
270
279
  # sono in minuscolo e separate da underscore.
271
280
  def self.transform_class_name(class_name)
272
- class_name.split("::").map(&:underscore).join("_")
281
+ class_name.split('::').map(&:underscore).join('_')
282
+ end
283
+
284
+ def self.load_data_from_file(seed_config)
285
+ config = seed_config
286
+ return unless config[:load_data] # Se load_data non è abilitato, esce senza fare nulla.
287
+
288
+ # Costruisce il nome del file di seed: ad esempio, se config[:file_name] è "my_model_seed",
289
+ # il file atteso sarà "my_model_seed_seed.sql".
290
+ seed_file_name = "#{config[:file_name]}.sql"
291
+ seed_file_path = File.join(BetterSeeder.configuration.preload_path, seed_file_name)
292
+
293
+ unless File.exist?(seed_file_path)
294
+ BetterSeeder::Utils.logger(message: "[WARN] Seed file not found: #{seed_file_path}")
295
+ return false
296
+ end
297
+
298
+ sql = File.read(seed_file_path)
299
+ begin
300
+ ActiveRecord::Base.connection.execute(sql)
301
+ BetterSeeder::Utils.logger(message: "[INFO] Loaded seed file: #{seed_file_path}")
302
+ true
303
+ rescue StandardError => e
304
+ BetterSeeder::Utils.logger(message: "[ERROR] Failed to load seed file: #{seed_file_path} - Error: #{e.message}")
305
+ false
306
+ end
273
307
  end
274
308
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/generators/better_seeder/structure_generator.rb
2
- require "rails/generators"
4
+ require 'rails/generators'
3
5
 
4
6
  module BetterSeeder
5
7
  class StructureGenerator < Rails::Generators::NamedBase
@@ -7,9 +9,9 @@ module BetterSeeder
7
9
  # source_root File.expand_path("templates", __dir__)
8
10
 
9
11
  def create_structure_file
10
- say_status("info", "Generating structure file for #{name}", :green)
12
+ say_status('info', "Generating structure file for #{name}", :green)
11
13
  file_path = BetterSeeder.generate_structure(model_name: name)
12
- say_status("info", "Structure file created at #{file_path}", :green)
14
+ say_status('info', "Structure file created at #{file_path}", :green)
13
15
  end
14
16
  end
15
- end
17
+ end
metadata CHANGED
@@ -1,31 +1,31 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_seeder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2.1
4
+ version: 0.2.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - alessio_bussolari
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-10 00:00:00.000000000 Z
11
+ date: 2025-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: ffaker
14
+ name: dry-schema
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.19'
19
+ version: '1.5'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '2.19'
26
+ version: '1.5'
27
27
  - !ruby/object:Gem::Dependency
28
- name: dry-schema
28
+ name: dry-types
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
@@ -39,19 +39,19 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.5'
41
41
  - !ruby/object:Gem::Dependency
42
- name: dry-types
42
+ name: ffaker
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.5'
47
+ version: '2.19'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.5'
54
+ version: '2.19'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: ruby-progressbar
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.11'
69
69
  - !ruby/object:Gem::Dependency
70
- name: activesupport
70
+ name: activerecord
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -81,7 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '4.2'
83
83
  - !ruby/object:Gem::Dependency
84
- name: activerecord
84
+ name: activesupport
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -151,6 +151,7 @@ metadata:
151
151
  homepage_uri: https://github.com/alessiobussolari/better_seeder
152
152
  source_code_uri: https://github.com/alessiobussolari/better_seeder
153
153
  changelog_uri: https://github.com/alessiobussolari/better_seeder/blob/main/CHANGELOG.md
154
+ rubygems_mfa_required: 'true'
154
155
  post_install_message:
155
156
  rdoc_options: []
156
157
  require_paths: