better_seeder 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7d791cd07db093b5651fff8ee43c8a9b04360efd6ad7aaa83978f9233f53f7a2
4
+ data.tar.gz: 4b4ca980a85b832b9261750db2742a89f61a18bc6a33f253a6486076a9a572ea
5
+ SHA512:
6
+ metadata.gz: 765e67ac2cac0af3e1b3a443f9896ce1212a0a6e0e6815b4bd7e7e148f75aabab3b6da279f2b5dc32c5a070c02024567524e175c710a5604e297221aa70157e8
7
+ data.tar.gz: a87f473eb71a6156dfdb829435323b67bcfe5645ebb30e32143071ba94107cdd99d730452032b6360c870212967d4d8e8c8b4f6fb496d6ba723384aa1e52e03e
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 alessiobussolari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # BetterSeeder
2
+
3
+ **BetterSeeder** is a Rails gem designed to simplify and centralize your application's seeding process. It provides a flexible system to generate dynamic data, validate it using Dry-schema, enforce uniqueness constraints, load it into the database, and export it in various formats (SQL, CSV, JSON). Configuration is centralized via a Rails initializer, while model-specific logic is defined in dedicated structure files.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - **Dynamic Data Generation**
10
+ Define custom data generators for each model in dedicated structure files.
11
+
12
+ - **Validation and Uniqueness**
13
+ Validate generated records using Dry-schema and enforce uniqueness constraints (both single and multi-column).
14
+
15
+ - **Loading & Exporting**
16
+ - Load generated data directly into your database (with support for parent/child relationships).
17
+ - Export data as a single SQL INSERT statement, or in CSV or JSON formats.
18
+
19
+ - **Centralized Configuration**
20
+ Easily customize settings such as `log_language`, `structure_path`, and `preload_path` via a Rails initializer. If no initializer is provided, default values are used.
21
+
22
+ - **Automatic Initializer Installation**
23
+ Use the `BetterSeeder.install` method to automatically create the initializer file in your Rails app.
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ Add the gem to your Gemfile:
30
+
31
+ ```ruby
32
+ gem 'better_seeder'
33
+ ```
34
+
35
+ Then run:
36
+
37
+ ```bash
38
+ bundle install
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Configuration
44
+
45
+ BetterSeeder uses a centralized configuration defined in `BetterSeeder.configuration`. You can override the default settings via an initializer. For example, create a file:
46
+
47
+ ```ruby
48
+ # config/initializers/better_seeder.rb
49
+ require 'better_seeder'
50
+
51
+ BetterSeeder.configure do |config|
52
+ config.log_language = :en
53
+ config.structure_path = Rails.root.join('db', 'seed', 'structure')
54
+ config.preload_path = Rails.root.join('db', 'seed', 'preload')
55
+ end
56
+ ```
57
+
58
+ If these values are set in the initializer, they will be used; otherwise, the gem will fall back to its default values.
59
+
60
+ ---
61
+
62
+ ## Install Method
63
+
64
+ BetterSeeder provides an `install` method that automatically creates an initializer file. To run the installer, simply execute in your Rails console:
65
+
66
+ ```ruby
67
+ BetterSeeder.install
68
+ ```
69
+
70
+ This command creates (if not already present) the file `config/initializers/better_seeder.rb` with content similar to:
71
+
72
+ ```ruby
73
+ # BetterSeeder initializer
74
+ BetterSeeder.configure do |config|
75
+ config.log_language = :en
76
+ config.structure_path = Rails.root.join('db', 'seed', 'structure')
77
+ config.preload_path = Rails.root.join('db', 'seed', 'preload')
78
+ end
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Structure Files
84
+
85
+ For each model, create a structure file that centralizes the logic for generating, validating, and configuring seed data. Each structure file should define at least the following methods:
86
+
87
+ - **`structure`**
88
+ Returns a hash where each key represents an attribute and its value is an array in the format `[type, lambda_generator]`.
89
+
90
+ - **`seed_schema` (Optional)**
91
+ Defines a Dry-schema for validating the generated records.
92
+
93
+ - **`seed_config`**
94
+ Returns a hash with model-specific seeding settings:
95
+ - `file_name`: The output file name (without extension)
96
+ - `columns: { excluded: [...] }`: Columns to exclude from the generated data
97
+ - `generate_data`: Boolean flag indicating whether to generate data dynamically (if false, existing records are used)
98
+ - `count`: The number of records to generate (default: 10)
99
+ - `load_data`: Boolean flag indicating whether the generated records should be inserted into the database
100
+ - `parent`: For child models, this specifies the parent model(s) used for injecting foreign keys
101
+
102
+ - **`unique_keys` (Optional)**
103
+ Returns an array of column groups (each group is an array of symbols) that must be unique.
104
+ For example:
105
+
106
+ ```ruby
107
+ def self.unique_keys
108
+ [[:email], [:first_name, :last_name]]
109
+ end
110
+ ```
111
+
112
+ ### Example Structure File
113
+
114
+ For a generic model `MyModel` in the namespace `MyNamespace`, create a file at `db/seed/structure/my_namespace/my_model_structure.rb`:
115
+
116
+ ```ruby
117
+ # db/seed/structure/my_namespace/my_model_structure.rb
118
+ module MyNamespace
119
+ class MyModelStructure < BetterSeeder::StructureBase
120
+ # Defines generators for each attribute.
121
+ def self.structure
122
+ {
123
+ name: [:string, -> { FFaker::Name.name }],
124
+ email: [:string, -> { FFaker::Internet.email }],
125
+ created_at: [:datetime, -> { Time.zone.now }]
126
+ }
127
+ end
128
+
129
+ # Optional: Validate generated records using Dry-schema.
130
+ def self.seed_schema
131
+ Dry::Schema.Params do
132
+ required(:name).filled(:string)
133
+ required(:email).filled(:string)
134
+ required(:created_at).filled(:time)
135
+ end
136
+ end
137
+
138
+ # Specific seeding configuration for MyModel.
139
+ def self.seed_config
140
+ {
141
+ file_name: 'my_model_seed',
142
+ columns: { excluded: [:id, :updated_at] },
143
+ generate_data: true,
144
+ count: 50,
145
+ load_data: true,
146
+ parent: nil
147
+ }
148
+ end
149
+
150
+ # Optional: Uniqueness constraints; for example, email must be unique.
151
+ def self.unique_keys
152
+ [[:email]]
153
+ end
154
+ end
155
+ end
156
+ ```
157
+
158
+ ---
159
+
160
+ ## How It Works
161
+
162
+ When you call `BetterSeeder.magic` with a configuration that contains an array of model names (as strings), the gem will:
163
+
164
+ 1. **Load Structure Files**
165
+ For each model, the gem loads the corresponding structure file from `BetterSeeder.configuration.structure_path`.
166
+
167
+ 2. **Retrieve Seeding Configurations**
168
+ It calls the model's `seed_config` method to get its specific settings.
169
+
170
+ 3. **Generate or Retrieve Records**
171
+ Using the `structure` method, the gem generates data dynamically (or retrieves existing records) and validates them with `seed_schema` if provided. Uniqueness is enforced based on `unique_keys`.
172
+
173
+ 4. **Handle Parent/Child Relationships**
174
+ For child models, foreign keys are automatically injected using the records from the parent models.
175
+
176
+ 5. **Load and Export**
177
+ If enabled (`load_data: true`), the generated records are inserted into the database and then exported in the specified format (SQL, CSV, or JSON). Export files are saved in the directory defined by `BetterSeeder.configuration.preload_path` (default: `db/seed/preload`).
178
+
179
+ ### Example Usage
180
+
181
+ ```ruby
182
+ BetterSeeder.magic(
183
+ {
184
+ configurations: { export_type: :sql },
185
+ data: [
186
+ 'MyNamespace::MyModel',
187
+ 'OtherNamespace::OtherModel'
188
+ ]
189
+ }
190
+ )
191
+ ```
192
+
193
+ This command processes each model by:
194
+
195
+ - Reading its structure file and retrieving its configuration via `seed_config`.
196
+ - Generating or fetching data according to the specified rules.
197
+ - Inserting the data into the database (if `load_data` is enabled).
198
+ - Exporting the data as an SQL file (or CSV/JSON, depending on `export_type`).
199
+
200
+ ---
201
+
202
+ ## Conclusion
203
+
204
+ BetterSeeder provides a modular, configurable, and extensible system for seeding your Rails application's data:
205
+
206
+ - **Centralized Configuration:**
207
+ Configure paths and logging via a Rails initializer.
208
+
209
+ - **Modular Structure Files:**
210
+ Define generation, validation, and configuration logic for each model in dedicated structure files.
211
+
212
+ - **Seamless Data Handling:**
213
+ Automatically generate, validate, load, and export seed data with support for parent/child relationships and various export formats.
214
+
215
+ For further details or contributions, please refer to the official repository or documentation.
@@ -0,0 +1,38 @@
1
+ # lib/better_seeder/configuration.rb
2
+
3
+ module BetterSeeder
4
+ class Configuration
5
+ attr_accessor :log_language, :structure_path, :preload_path
6
+
7
+ def initialize
8
+ if defined?(Rails) && Rails.respond_to?(:root)
9
+ @log_language = :en
10
+ @structure_path = Rails.root.join('db', 'seed', 'structure')
11
+ @preload_path = Rails.root.join('db', 'seed', 'preload')
12
+ else
13
+ @log_language = :en
14
+ @structure_path = File.join(Dir.pwd, 'db', 'seed', 'structure')
15
+ @preload_path = File.join(Dir.pwd, 'db', 'seed', 'preload')
16
+ end
17
+ end
18
+ end
19
+
20
+ # Restituisce l'istanza globale di configurazione. Se non esiste, viene creata con i valori di default.
21
+ def self.configuration
22
+ @configuration ||= Configuration.new
23
+ end
24
+
25
+ # Permette di configurare BetterSeeder tramite un blocco. Ad esempio, in config/initializers/better_seeder.rb:
26
+ #
27
+ # BetterSeeder.configure do |config|
28
+ # config.log_language = :en
29
+ # config.structure_path = Rails.root.join('db', 'seed', 'structure')
30
+ # config.preload_path = Rails.root.join('db', 'seed', 'preload')
31
+ # end
32
+ #
33
+ # Se questo blocco viene eseguito, i valori della configurazione verranno aggiornati; altrimenti,
34
+ # verranno utilizzati quelli definiti nel costruttore (default).
35
+ def self.configure
36
+ yield(configuration)
37
+ end
38
+ end
@@ -0,0 +1,65 @@
1
+ # lib/better_seeder/exporter/base_exporter.rb
2
+
3
+ module BetterSeeder
4
+ module Exporters
5
+ class Base
6
+ # I dati da esportare.
7
+ # output_path: nome del file (senza estensione) da concatenare al preload_path configurato.
8
+ # table_name: (opzionale) nome della tabella, utile ad esempio per SqlExporter.
9
+ attr_reader :data, :output_path, :table_name
10
+
11
+ # Inizializza l'exporter.
12
+ #
13
+ # Esempio d'uso:
14
+ # data = [
15
+ # { id: 1, name: "Alice", email: "alice@example.com" },
16
+ # { id: 2, name: "Bob", email: "bob@example.com" }
17
+ # ]
18
+ #
19
+ # json_exporter = BetterSeeder::Performers::JsonExporter.new(data, output_path: 'users', table_name: 'users')
20
+ # json_exporter.export
21
+ #
22
+ # @param data [Array<Hash>] I dati da esportare.
23
+ # @param output_path [String] Nome del file (senza estensione).
24
+ # @param table_name [String] Nome della tabella (usato in SqlExporter).
25
+ def initialize(data, output_path:, table_name: 'my_table')
26
+ @data = data
27
+ # Utilizza il preload_path definito nella configurazione BetterSeeder (impostato nell'initializer).
28
+ @output_path = File.join(BetterSeeder.configuration.preload_path, output_path)
29
+ @table_name = table_name
30
+ end
31
+
32
+ # Restituisce la directory in cui salvare i file.
33
+ # In questo caso, utilizza la configurazione BetterSeeder.configuration.preload_path.
34
+ #
35
+ # @return [String] il percorso della directory di output.
36
+ def output_directory
37
+ BetterSeeder.configuration.preload_path.to_s
38
+ end
39
+
40
+ # Verifica che la directory di output esista; se non esiste, la crea.
41
+ def ensure_output_directory
42
+ FileUtils.mkdir_p(output_directory) unless Dir.exist?(output_directory)
43
+ end
44
+
45
+ # Costruisce il percorso completo del file di output, combinando la directory, l'output_path e l'estensione.
46
+ #
47
+ # @return [String] il percorso completo del file.
48
+ def full_output_path
49
+ ensure_output_directory
50
+ "#{output_path}#{extension}"
51
+ end
52
+
53
+ # Metodo astratto per ottenere l'estensione del file (es. ".json", ".csv", ".sql").
54
+ # Le classi derivate devono implementarlo.
55
+ def extension
56
+ raise NotImplementedError, "Subclasses must implement #extension"
57
+ end
58
+
59
+ # Metodo astratto per effettuare l'export.
60
+ def export
61
+ raise NotImplementedError, "Subclasses must implement the export method"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,23 @@
1
+ module BetterSeeder
2
+ module Exporters
3
+ class Csv < Base
4
+ # Esporta i dati in formato CSV e li salva in un file nella cartella "db/seed/preload".
5
+ # Se la cartella non esiste, viene creata automaticamente.
6
+ def export
7
+ return if data.empty?
8
+ headers = data.first.keys
9
+
10
+ # Costruisce il percorso completo del file di output
11
+ full_path = File.join(full_output_path)
12
+
13
+ CSV.open(full_path, 'w', write_headers: true, headers: headers) do |csv|
14
+ data.each { |row| csv << row.values }
15
+ end
16
+ end
17
+
18
+ def extension
19
+ '.csv'
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module BetterSeeder
2
+ module Exporters
3
+ class Json < Base
4
+ # Esporta i dati in formato JSON e li salva in un file nella cartella "db/seed/preload".
5
+ # Se la cartella non esiste, viene creata automaticamente.
6
+ def export
7
+ # Imposta la directory di output
8
+ full_path = File.join(full_output_path)
9
+
10
+ File.open(full_path, 'w') do |file|
11
+ file.write(JSON.pretty_generate(data))
12
+ end
13
+ end
14
+
15
+ def extension
16
+ '.json'
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ module BetterSeeder
2
+ module Exporters
3
+ class Sql < Base
4
+ # Esporta i dati in formato SQL, generando una singola istruzione INSERT
5
+ # che inserisce in blocco tutti i record.
6
+ #
7
+ # Il metodo costruisce una stringa con la sintassi:
8
+ # INSERT INTO table_name (col1, col2, ...) VALUES
9
+ # (val11, val12, ...),
10
+ # (val21, val22, ...),
11
+ # ... ;
12
+ def export
13
+ return if data.empty?
14
+ columns = data.first.keys
15
+
16
+ # Crea l'array delle tuple di valori per ciascun record.
17
+ values_list = data.map do |row|
18
+ row_values = columns.map do |col|
19
+ value = row[col]
20
+ # Se il valore è nil restituisce NULL, altrimenti esegue l'escaping delle virgolette singole.
21
+ value.nil? ? "NULL" : "'#{value.to_s.gsub("'", "''")}'"
22
+ end
23
+ "(#{row_values.join(', ')})"
24
+ end
25
+
26
+ # Costruisce la query INSERT unica
27
+ insert_statement = "INSERT INTO #{table_name} (#{columns.join(', ')}) VALUES #{values_list.join(', ')};"
28
+
29
+ full_path = File.join(full_output_path)
30
+
31
+ File.open(full_path, 'w') do |file|
32
+ file.puts(insert_statement)
33
+ end
34
+ end
35
+
36
+ def extension
37
+ '.sql'
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,117 @@
1
+ module BetterSeeder
2
+ module Generators
3
+ class DataGenerator
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
43
+ end
44
+
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)
64
+ end
65
+ { columns: cols, set: existing_keys }
66
+ end
67
+
68
+ generated_records = []
69
+ progressbar = ProgressBar.create(total: count, format: '%a %B %p%% %t')
70
+ attempts = 0
71
+
72
+ # Continua a generare record finché non si raggiunge il numero richiesto.
73
+ while generated_records.size < count
74
+ attempts += 1
75
+ record = {}
76
+ 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
80
+ record[attribute] = value
81
+ end
82
+
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.
91
+ end
92
+ end
93
+
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
100
+
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)
105
+ end
106
+
107
+ generated_records << record
108
+ progressbar.increment
109
+ end
110
+
111
+ message = "[INFO] Generated #{generated_records.size} unique records for #{model_name} after #{attempts} attempts."
112
+ BetterSeeder::Utils.logger(message: message)
113
+ generated_records
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,15 @@
1
+ require 'dry-types'
2
+
3
+ module BetterSeeder
4
+ # Superclasse per tutte le structures.
5
+ # Fornisce il modulo Types da utilizzare per la definizione dei tipi.
6
+ module Structure
7
+ class Utils
8
+ module Types
9
+ include Dry.Types()
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ BetterSeederTypes = BetterSeeder::Structure::Utils::Types
@@ -0,0 +1,22 @@
1
+ # lib/better_seeder/utils.rb
2
+
3
+ module BetterSeeder
4
+ 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
+
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
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterSeeder
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,269 @@
1
+ require_relative "better_seeder/utils"
2
+ require_relative "better_seeder/configuration"
3
+ require_relative "better_seeder/structure/utils"
4
+ require_relative "better_seeder/generators/data_generator"
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
+
10
+ module BetterSeeder
11
+ class Configuration
12
+ attr_accessor :log_language, :structure_path, :preload_path
13
+
14
+ def initialize
15
+ if defined?(Rails) && Rails.respond_to?(:root)
16
+ @log_language = :en
17
+ @structure_path = Rails.root.join('db', 'seed', 'structure')
18
+ @preload_path = Rails.root.join('db', 'seed', 'preload')
19
+ else
20
+ @log_language = :en
21
+ @structure_path = File.join(Dir.pwd, 'db', 'seed', 'structure')
22
+ @preload_path = File.join(Dir.pwd, 'db', 'seed', 'preload')
23
+ end
24
+ end
25
+ end
26
+
27
+ # Restituisce l'istanza globale di configurazione. Se non esiste, viene creata con i valori di default.
28
+ def self.configuration
29
+ @configuration ||= Configuration.new
30
+ end
31
+
32
+ # Permette di configurare BetterSeeder tramite un blocco. Ad esempio, in config/initializers/better_seeder.rb:
33
+ #
34
+ # BetterSeeder.configure do |config|
35
+ # config.log_language = :en
36
+ # config.structure_path = Rails.root.join('db', 'seed', 'structure')
37
+ # config.preload_path = Rails.root.join('db', 'seed', 'preload')
38
+ # end
39
+ #
40
+ # Se questo blocco viene eseguito, i valori della configurazione verranno aggiornati; altrimenti,
41
+ # verranno utilizzati quelli definiti nel costruttore (default).
42
+ def self.configure
43
+ yield(configuration)
44
+ end
45
+
46
+ # Metodo install che crea l'initializer di BetterSeeder in config/initializers
47
+ # con le seguenti impostazioni:
48
+ # - log_language: lingua da usare per i log (es. :en, :it)
49
+ # - structure_path: percorso dove sono memorizzati i file di structure (default: Rails.root/db/seed/structure)
50
+ # - preload_path: percorso dove verranno salvati i file esportati (default: Rails.root/db/seed/preload)
51
+ def self.install
52
+ initializer_path = File.join(Rails.root, "config", "initializers", "better_seeder.rb")
53
+
54
+ if File.exist?(initializer_path)
55
+ message = "BetterSeeder initializer already exists at #{initializer_path}"
56
+ else
57
+ content = <<~RUBY
58
+ # BetterSeeder initializer
59
+ # This file was generated by BetterSeeder.install
60
+ BetterSeeder.configure do |config|
61
+ config.log_language = :en
62
+ config.structure_path = Rails.root.join('db', 'seed', 'structure')
63
+ config.preload_path = Rails.root.join('db', 'seed', 'preload')
64
+ end
65
+ RUBY
66
+
67
+ FileUtils.mkdir_p(File.dirname(initializer_path))
68
+ File.write(initializer_path, content)
69
+ message = "BetterSeeder initializer created at #{initializer_path}"
70
+ end
71
+
72
+ # Use Rails.logger if available, otherwise fallback to STDOUT.
73
+ BetterSeeder::Utils.logger(message: message)
74
+ end
75
+
76
+ # Metodo master della gemma.
77
+ #
78
+ # La configurazione attesa è un hash con la seguente struttura:
79
+ #
80
+ # {
81
+ # configurations: { export_type: :sql },
82
+ # data: [
83
+ # 'Campaigns::Campaign',
84
+ # 'Creators::Creator',
85
+ # 'Media::Media',
86
+ # 'Media::Participant'
87
+ # ]
88
+ # }
89
+ #
90
+ # Per ciascun modello (identificato da stringa), viene:
91
+ # - Caricato il file di structure relativo, che definisce la configurazione specifica tramite `seed_config`
92
+ # - Recuperati (o generati) i record, con eventuali controlli di esclusione e iniezione di foreign key per modelli child
93
+ # - Se abilitato, i record vengono caricati nel database e successivamente esportati nel formato richiesto
94
+ # - Vengono raccolte statistiche e loggato il tempo totale di esecuzione
95
+ def self.magic(config)
96
+ start_time = Time.now
97
+ stats = {} # Statistiche: modello => numero di record caricati
98
+ parent_loaded_records = {} # Per memorizzare i record creati per i modelli parent
99
+
100
+ ActiveRecord::Base.transaction do
101
+ export_type = config[:configurations][:export_type]
102
+ model_names = config[:data]
103
+ model_names.each do |model_name|
104
+ process_config(model_name, export_type, stats, parent_loaded_records)
105
+ end
106
+ end
107
+
108
+ total_time = Time.now - start_time
109
+ log_statistics(stats, total_time)
110
+ end
111
+
112
+ private
113
+
114
+ # Processa la configurazione per un singolo modello.
115
+ # Carica il file di structure corrispondente e recupera la configurazione tramite `seed_config`.
116
+ # Quindi, esegue il recupero o la generazione dei record, iniezione di eventuali foreign key per modelli child,
117
+ # caricamento nel database (se abilitato) ed esportazione dei dati.
118
+ def self.process_config(model_name, export_type, stats, parent_loaded_records)
119
+ # Costruisce il percorso del file di structure in base al nome del modello.
120
+ # Esempio: per "Campaigns::Campaign", si attende "db/seed/structure/campaigns/campaign_structure.rb"
121
+ structure_file = File.expand_path(
122
+ File.join(BetterSeeder.configuration.structure_path, "#{model_name.underscore}_structure.rb"),
123
+ Dir.pwd
124
+ )
125
+ raise "Structure file not found: #{structure_file}" unless File.exist?(structure_file)
126
+
127
+ # Carica il file di structure.
128
+ load structure_file
129
+
130
+ # Il nome della classe di structure viene ottenuto semplicemente concatenando "Structure" al nome del modello.
131
+ # Es: "Campaigns::Campaign" => "Campaigns::CampaignStructure"
132
+ structure_class_name = "#{model_name}Structure"
133
+ begin
134
+ structure_class = Object.const_get(structure_class_name)
135
+ rescue error
136
+ message = "[ERROR] Structure class not found: #{structure_class_name}"
137
+ BetterSeeder::Utils.logger(message: message)
138
+ raise error
139
+ end
140
+
141
+ # Recupera la configurazione specifica dal file di structure tramite il metodo seed_config.
142
+ # Se non definito, vengono usati dei valori di default.
143
+ seed_config = structure_class.respond_to?(:seed_config) ? structure_class.seed_config : {}
144
+ file_name = seed_config[:file_name] || "#{model_name.underscore}_seed"
145
+ excluded_columns = seed_config.dig(:columns, :excluded) || []
146
+ generate_data = seed_config.fetch(:generate_data, true)
147
+ count = seed_config[:count] || 10
148
+ load_data = seed_config.fetch(:load_data, true)
149
+ parent = seed_config[:parent] # nil oppure valore (o array) per modelli child
150
+
151
+ # Log per indicare se il modello è parent o child.
152
+ message = if parent.nil?
153
+ "[INFO] Processing parent model #{model_name}"
154
+ else
155
+ "[INFO] Processing child model #{model_name} (parent: #{parent.inspect})"
156
+ end
157
+ BetterSeeder::Utils.logger(message: message)
158
+
159
+ # Recupera la classe reale del modello (ActiveRecord).
160
+ model_class = Object.const_get(model_name) rescue nil
161
+ unless model_class
162
+ message = "[ERROR] Model #{model_name} not found."
163
+ BetterSeeder::Utils.logger(message: message)
164
+
165
+ raise Object.const_get(model_name)
166
+ end
167
+
168
+ # Recupera o genera i record.
169
+ records = if generate_data
170
+ Generators::DataGenerator.generate(model: model_name, count: count)
171
+ else
172
+ model_class.all.map(&:attributes)
173
+ end
174
+
175
+ # Rimuove le colonne escluse.
176
+ processed_records = records.map do |record|
177
+ record.reject { |key, _| excluded_columns.include?(key.to_sym) }
178
+ end
179
+
180
+ # Se il modello è child, inietta le foreign key.
181
+ if parent
182
+ Array(parent).each do |parent_model_name|
183
+ parent_records = parent_loaded_records[parent_model_name]
184
+ if parent_records.nil? || parent_records.empty?
185
+ message = "[ERROR] No loaded records found for parent model #{parent_model_name}. Cannot assign foreign key for #{model_name}."
186
+ BetterSeeder::Utils.logger(message: message)
187
+ else
188
+ # Il nome della foreign key è ottenuto prendendo l'ultima parte del nome del modello padre,
189
+ # trasformandola in minuscolo e in forma singolare, e aggiungendo "_id".
190
+ foreign_key = parent_model_name.split("::").last.underscore.singularize + "_id"
191
+ processed_records.each do |record|
192
+ record[foreign_key] = parent_records.sample.id
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ # Se abilitato, carica i record nel database.
199
+ if load_data
200
+ total_records = processed_records.size
201
+ stats[model_name] = total_records
202
+ created_records = load_records_into_db(model_class, processed_records, total_records, model_name)
203
+ # Se il modello è parent, salva i record creati per poterli utilizzare in seguito per i modelli child.
204
+ parent_loaded_records[model_name] = created_records if parent.nil?
205
+ else
206
+ stats[model_name] = 0
207
+ end
208
+
209
+ # Esporta i record nel formato richiesto.
210
+ export_records(model_class, processed_records, export_type, file_name)
211
+ end
212
+
213
+ # Carica i record nel database, utilizzando una progress bar per monitorare il progresso.
214
+ # I log delle query SQL vengono temporaneamente disabilitati.
215
+ #
216
+ # @return [Array<Object>] Array dei record creati (istanze ActiveRecord)
217
+ def self.load_records_into_db(model_class, processed_records, total_records, model_name)
218
+ created_records = []
219
+ progressbar = ProgressBar.create(total: total_records, format: '%a %B %p%% %t')
220
+ message = "[INFO] Starting to load #{total_records} records for model #{model_name}..."
221
+ BetterSeeder::Utils.logger(message: message)
222
+
223
+ previous_level = ActiveRecord::Base.logger.level
224
+ ActiveRecord::Base.logger.level = Logger::ERROR
225
+
226
+ processed_records.each do |record|
227
+ created = model_class.create!(record)
228
+ created_records << created
229
+ progressbar.increment
230
+ end
231
+
232
+ ActiveRecord::Base.logger.level = previous_level
233
+ message = "[INFO] Finished loading #{total_records} records into model #{model_name}."
234
+ BetterSeeder::Utils.logger(message: message)
235
+ created_records
236
+ end
237
+
238
+ # Esporta i record nel formato specificato (json, csv, sql).
239
+ def self.export_records(model_class, processed_records, export_type, file_name)
240
+ exporter = case export_type.to_s.downcase
241
+ when 'json'
242
+ Exporters::Json.new(processed_records, output_path: file_name)
243
+ when 'csv'
244
+ Exporters::Csv.new(processed_records, output_path: file_name)
245
+ when 'sql'
246
+ table_name = model_class.respond_to?(:table_name) ? model_class.table_name : transform_class_name(model_class.name)
247
+ Exporters::Sql.new(processed_records, output_path: file_name, table_name: table_name)
248
+ else
249
+ raise ArgumentError, "Unsupported export type: #{export_type}"
250
+ end
251
+
252
+ exporter.export
253
+ message = "[INFO] Exported data for #{model_class.name} to #{file_name}"
254
+ BetterSeeder::Utils.logger(message: message)
255
+ end
256
+
257
+ # Log finale con le statistiche raccolte e il tempo totale di esecuzione.
258
+ def self.log_statistics(stats, total_time)
259
+ stats_message = stats.map { |model, count| "#{model}: #{count} records" }.join(", ")
260
+ message = "[INFO] Finished processing all models in #{total_time.round(2)} seconds. Statistics: #{stats_message}"
261
+ BetterSeeder::Utils.logger(message: message)
262
+ end
263
+
264
+ # Metodo di utilità per trasformare il nome della classe in un formato in cui le lettere
265
+ # sono in minuscolo e separate da underscore.
266
+ def self.transform_class_name(class_name)
267
+ class_name.split("::").map(&:underscore).join("_")
268
+ end
269
+ end
metadata ADDED
@@ -0,0 +1,171 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: better_seeder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - alessio_bussolari
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-02-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffaker
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.19'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.19'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-schema
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-types
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby-progressbar
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.11'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.11'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '4.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '4.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activerecord
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.2'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '13.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '13.0'
125
+ description: A Rails gem that provides simple methods to optimize and maintain seed
126
+ data, making seeding more efficient and your code more maintainable and performant.
127
+ email:
128
+ - alessio.bussolari@pandev.it
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - LICENSE.txt
134
+ - README.md
135
+ - lib/better_seeder.rb
136
+ - lib/better_seeder/configuration.rb
137
+ - lib/better_seeder/exporters/base.rb
138
+ - lib/better_seeder/exporters/csv.rb
139
+ - lib/better_seeder/exporters/json.rb
140
+ - lib/better_seeder/exporters/sql.rb
141
+ - lib/better_seeder/generators/data_generator.rb
142
+ - lib/better_seeder/structure/utils.rb
143
+ - lib/better_seeder/utils.rb
144
+ - lib/better_seeder/version.rb
145
+ homepage: https://github.com/alessiobussolari/better_seeder
146
+ licenses:
147
+ - MIT
148
+ metadata:
149
+ homepage_uri: https://github.com/alessiobussolari/better_seeder
150
+ source_code_uri: https://github.com/alessiobussolari/better_seeder
151
+ changelog_uri: https://github.com/alessiobussolari/better_seeder/blob/main/CHANGELOG.md
152
+ post_install_message:
153
+ rdoc_options: []
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: 3.0.0
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ requirements: []
167
+ rubygems_version: 3.5.11
168
+ signing_key:
169
+ specification_version: 4
170
+ summary: 'BetterSeeder: Simplify and optimize seeding.'
171
+ test_files: []