seedie 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: 561377deea81325062cee19ab2cd29e48d6d434e2872b97474712c1ab07a4d87
4
+ data.tar.gz: 06a8f58b9488ec48da96b6678696bbfc672fd71f881395a795a8a97eff4d1478
5
+ SHA512:
6
+ metadata.gz: eda91d6bd4be5e638ef13e963d517f25055e9a96bbccfd222d565caeed5eddaf929fff9742a45bc260c27cafb5c2f0d37e4d91948c1d7786e17cc10202c05289
7
+ data.tar.gz: 18f0f86fadc97d6e9be4a736d0795cdf09a6a8df044fd5f6d1a7ae4d1e3f0c75a3be706111b56c5c44bacfaf3f7d072fc5479091f412be6f319591b83616e8c8
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,16 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+ DisabledByDefault: true
4
+
5
+ Style/StringLiterals:
6
+ Enabled: true
7
+ EnforcedStyle: double_quotes
8
+
9
+ Style/StringLiteralsInInterpolation:
10
+ Enabled: true
11
+ EnforcedStyle: double_quotes
12
+
13
+ Layout/LineLength:
14
+ Max: 120
15
+ Exclude:
16
+ - "spec/dummy/**/*"
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in seedie.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+ gem "rubocop", "~> 1.21"
10
+
11
+ gem "pry", "~> 0.14.2"
12
+
13
+ group :test do
14
+ gem "simplecov", "~> 0.22.0", require: false
15
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Keshav Biswa
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,111 @@
1
+ # Seedie
2
+
3
+ Seedie is a Ruby gem designed to streamline the seeding of your database.
4
+ Utilizing the Faker library, Seedie generates realistic data for ActiveRecord models.
5
+ Currently supports only PostrgreSQL and SQLite3 databases.
6
+ The gem includes a Rake task for seeding models and a Rails generator for easy setup.
7
+
8
+ ## Installation
9
+
10
+ Add the following line to your application's Gemfile:
11
+
12
+ ```bash
13
+ gem 'seedie'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ ```bash
19
+ $ bundle install
20
+ ```
21
+
22
+ Or install it yourself as:
23
+
24
+ ```bash
25
+ $ gem install seedie
26
+ ```
27
+ Next, run the install generator:
28
+
29
+ ```bash
30
+ $ rails generate seedie:install
31
+ ```
32
+ This will create a seedie.yml file in your config directory, which will include configurations for your models.
33
+
34
+ ## Usage
35
+
36
+ To seed your models, run the following Rake task:
37
+
38
+ ```bash
39
+ $ rake seedie:seed
40
+ ```
41
+
42
+ This will use the configurations specified in seedie.yml to seed your models.
43
+
44
+ The seedie.yml file has entries for each model in your application, and you can customize the configuration for each one.
45
+
46
+ Here's an example of a more advanced configuration in seedie.yml:
47
+
48
+
49
+ ```yaml
50
+ default_count: 5
51
+ models:
52
+ user:
53
+ attributes:
54
+ name: "name {{index}}"
55
+ email: "{{Faker::Internet.email}}"
56
+ address: "{{Faker::Address.street_address}}"
57
+ disabled_fields: [nickname password password_digest]
58
+ post: &post
59
+ count: 2
60
+ attributes:
61
+ title: "title {{index}}"
62
+ associations:
63
+ has_many:
64
+ comments: 4
65
+ belongs_to:
66
+ user: random
67
+ has_one:
68
+ post_metadatum:
69
+ attributes:
70
+ seo_text: "{{Faker::Lorem.paragraph}}"
71
+ disabled_fields: []
72
+ comment:
73
+ attributes:
74
+ title: "title {{index}}"
75
+ associations:
76
+ belongs_to:
77
+ post:
78
+ attributes:
79
+ title: "Comment Post {{index}}"
80
+
81
+ ```
82
+
83
+ In this file:
84
+
85
+ - `default_count` specifies the number of records to be generated for each model when no specific count is provided in the model's configuration.
86
+ - `models` is a hash that contains a configuration for each model that should be seeded.
87
+ - `attributes` is a hash that maps field names to the values that should be used. If attributes are not defined, Seedie will use Faker to generate a value for the field.
88
+ - The special `{{index}}` placeholder will be replaced by the index of the current record being created, starting from 1. This allows you to have unique values for each record.
89
+ - Additionally, we can use placeholders like `{{Faker::Internet.email}}` to generate dynamic and unique data for each record using Faker.
90
+ - `disabled_fields` is an array of fields that should not be automatically filled by Seedie.
91
+ - `associations` specify how associated models should be generated. Here, `has_many`, `belongs_to`, and `has_one` are supported.
92
+ - The specified number for `has_many` represents the number of associated records to create.
93
+ - For `belongs_to`, the value `random` means that a random existing record will be associated.
94
+ - If attributes are specified under an association, those attributes will be used when creating the associated record(s)
95
+ - When using associations, it's important to define the models in the correct order in the `seedie.yml` file. Associated models should be defined before the models that reference them.
96
+
97
+ ## Development
98
+
99
+ After checking out the repo, run `bin/setup` to install dependencies.
100
+ Then, run `bundle exec rspec` to run the tests.
101
+ By default, the tests will supress output of the seeds progress.
102
+ Use `DEBUG_OUTPUT=true bundle exec rspec` to see the output of the seeds.
103
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
104
+
105
+ ## Contributing
106
+
107
+ Bug reports and pull requests are welcome on GitHub at https://github.com/keshavbiswa/seedie.
108
+
109
+ ## License
110
+
111
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+
6
+ desc "Run all examples"
7
+ RSpec::Core::RakeTask.new(:spec) do |t|
8
+ t.ruby_opts = %w[-w]
9
+ t.rspec_opts = %w[--color]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[spec rubocop]
@@ -0,0 +1,7 @@
1
+ Description:
2
+ Creates a seedie.yml file in your config directory.
3
+ Example:
4
+ bin/rails generate seedie:install
5
+
6
+ This will create:
7
+ config/seedie.yml
@@ -0,0 +1,136 @@
1
+ require "rails/generators/base"
2
+ require "active_record"
3
+
4
+ module Seedie
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ EXCLUDED_MODELS = %w[
8
+ ActiveRecord::SchemaMigration
9
+ ActiveRecord::InternalMetadata
10
+ ActiveStorage::Attachment
11
+ ActiveStorage::Blob
12
+ ActiveStorage::VariantRecord
13
+ ActionText::RichText
14
+ ActionMailbox::InboundEmail
15
+ ActionText::EncryptedRichText
16
+ ]
17
+
18
+ source_root File.expand_path("templates", __dir__)
19
+
20
+ desc "Creates a seedie.yml for your application."
21
+ def generate_seedie_file(output = STDOUT)
22
+ Rails.application.eager_load! # Load all models. This is required!!
23
+
24
+ @models = get_models
25
+ @models_config = build_models_config
26
+ template "seedie.yml", "config/seedie.yml"
27
+ output_seedie_warning(output)
28
+ end
29
+
30
+ private
31
+
32
+
33
+ def build_models_config
34
+ models = Model::ModelSorter.new(@models).sort_by_dependency
35
+ models.reduce({}) do |config, model|
36
+ config[model.name.underscore] = model_configuration(model)
37
+ config
38
+ end
39
+ end
40
+
41
+ def model_configuration(model)
42
+ attributes_configuration(model).merge(associations_configuration(model))
43
+ end
44
+
45
+ def attributes_configuration(model)
46
+ active_columns = []
47
+ disabled_columns = []
48
+
49
+ model.columns.each do |column|
50
+ # Excluding DEFAULT_DISABLED_FIELDS
51
+ # Excluding foreign_keys, polymorphic associations,
52
+ # password digest, columns with default functions or values
53
+ next if ModelFields::DEFAULT_DISABLED_FIELDS.include?(column.name)
54
+ next if column.name.end_with?("_id", "_type", "_digest")
55
+ next if column.default_function.present?
56
+ next if column.default.present?
57
+
58
+ # Only add to active if its required or has presence validator
59
+ if column.null == false || has_presence_validator?(model, column.name)
60
+ active_columns << column
61
+ else
62
+ disabled_columns << column
63
+ end
64
+ end
65
+
66
+ # Add atleast one column to active columns
67
+ active_columns << disabled_columns.pop if active_columns.empty? && disabled_columns.present?
68
+
69
+ {
70
+ "attributes" => active_columns_configuration(model, active_columns),
71
+ "disabled_fields" => disabled_columns_configuration(disabled_columns)
72
+ }
73
+ end
74
+
75
+ def active_columns_configuration(model, columns)
76
+ columns.reduce({}) do |config, column|
77
+ validations = model.validators_on(column.name)
78
+ config[column.name] = if validations.present?
79
+ FieldValues::FakerBuilder.new(column.name, column, validations).build_faker_constant
80
+ else
81
+ FieldValues::FakeValue.new(column.name, column).generate_fake_value
82
+ end
83
+ config
84
+ end
85
+ end
86
+
87
+ def disabled_columns_configuration(disabled_columns)
88
+ disabled_columns.map(&:name)
89
+ end
90
+
91
+ def associations_configuration(model)
92
+ {
93
+ "associations" => {
94
+ "belongs_to" => belongs_to_associations_configuration(model),
95
+ "has_one" => {}, # TODO: Add has_one associations
96
+ "has_many" => {}, # TODO: Add has_many associations
97
+ }
98
+ }
99
+ end
100
+
101
+ def belongs_to_associations_configuration(model)
102
+ belongs_to_associations = model.reflect_on_all_associations(:belongs_to).reject do |association|
103
+ association.options[:polymorphic] == true || # Excluded Polymorphic Associations
104
+ association.options[:optional] == true # Excluded Optional Associations
105
+ end
106
+
107
+ belongs_to_associations.reduce({}) do |config, association|
108
+ config[association.name.to_s] = "random"
109
+ config
110
+ end
111
+ end
112
+
113
+ def has_presence_validator?(model, column_name)
114
+ model.validators_on(column_name).any? { |v| v.kind == :presence }
115
+ end
116
+
117
+ def get_models
118
+ @get_models ||= ActiveRecord::Base.descendants.reject do |model|
119
+ EXCLUDED_MODELS.include?(model.name) || # Excluded Reserved Models
120
+ model.abstract_class? || # Excluded Abstract Models
121
+ model.table_exists? == false || # Excluded Models without tables
122
+ model.name.blank? || # Excluded Anonymous Models
123
+ model.name.start_with?("HABTM_") # Excluded HABTM Models
124
+ end
125
+ end
126
+
127
+ def output_seedie_warning(output)
128
+ output.puts "Seedie config file generated at config/seedie.yml"
129
+ output.puts "##################################################"
130
+ output.puts "WARNING: Please review the generated config file before running the seeds."
131
+ output.puts "There might be some things that you might need to change to ensure that the generated seeds run successfully."
132
+ output.puts "##################################################"
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,26 @@
1
+ default_count: 5
2
+ models:
3
+ <% @models_config.each do |model, config| -%>
4
+ <%= model %>:
5
+ attributes:
6
+ <% config['attributes'].each do |name, value| -%>
7
+ <% if value.is_a?(Hash) && value.key?("custom_attr_value") -%>
8
+ <%= name %>:
9
+ custom_attr_value:
10
+ values: <%= value["custom_attr_value"]["values"] %>
11
+ pick_strategy: <%= value["custom_attr_value"]["pick_strategy"] %>
12
+ <% else -%>
13
+ <%= name %>: '<%= value %>'
14
+ <% end -%>
15
+ <% end -%>
16
+ disabled_fields: <%= config['disabled_fields'] %>
17
+ <% if config['associations'] -%>
18
+ associations:
19
+ <% config['associations'].each do |type, associations| -%>
20
+ <%= type %>:
21
+ <% associations.each do |association, value| -%>
22
+ <%= association %>: <%= value %>
23
+ <% end -%>
24
+ <% end -%>
25
+ <% end -%>
26
+ <% end -%>
@@ -0,0 +1,44 @@
1
+ module Seedie
2
+ class BaseAssociation
3
+ include Reporters::Reportable
4
+
5
+ DEFAULT_COUNT = 1
6
+ INDEX = 0
7
+
8
+ attr_reader :record, :model, :association_config, :reporters
9
+
10
+ def initialize(record, model, association_config, reporters = [])
11
+ @record = record
12
+ @model = model
13
+ @association_config = association_config
14
+ @reporters = reporters
15
+
16
+ add_observers(@reporters)
17
+ end
18
+
19
+ def generate_associations
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def generate_association
24
+ raise NotImplementedError
25
+ end
26
+
27
+ private
28
+
29
+ def get_association_count(config)
30
+ return config if only_count_given?(config)
31
+ return config["count"] if config["count"].present?
32
+
33
+ DEFAULT_COUNT
34
+ end
35
+
36
+ def only_count_given?(config)
37
+ config.is_a?(Numeric) || config.is_a?(String)
38
+ end
39
+
40
+ def generate_associated_field(id, association_name)
41
+ { "#{association_name}" => id }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,89 @@
1
+ module Seedie
2
+ module Associations
3
+ class BelongsTo < BaseAssociation
4
+ attr_reader :associated_field_set
5
+
6
+ def initialize(model, association_config, reporters = [])
7
+ super(nil, model, association_config, reporters)
8
+
9
+ @associated_field_set = {}
10
+ end
11
+
12
+ def generate_associations
13
+ return if association_config["belongs_to"].nil?
14
+
15
+ report(:belongs_to_start)
16
+
17
+ association_config["belongs_to"].each do |association_name, association_config|
18
+ reflection = model.reflect_on_association(association_name)
19
+
20
+ handle_association_config_type(reflection, association_name, association_config)
21
+ end
22
+ end
23
+
24
+ def generate_association(klass, config, index)
25
+ field_values_set = FieldValuesSet.new(klass, config, index).generate_field_values
26
+
27
+ Model::Creator.new(klass).create!(field_values_set)
28
+ end
29
+
30
+ private
31
+
32
+ def handle_association_config_type(reflection, association_name, association_config)
33
+ case get_type(association_config)
34
+ when "random"
35
+ handle_random_config_type(reflection)
36
+ when "unique"
37
+ handle_unique_config_type(reflection)
38
+ when "new"
39
+ handle_new_config_type(reflection)
40
+ else
41
+ handle_other_config_type(reflection, association_config)
42
+ end
43
+ end
44
+
45
+ def handle_random_config_type(reflection)
46
+ klass = reflection.klass
47
+ id = Model::IdGenerator.new(klass).random_id
48
+
49
+ report(:random_association, name: klass.to_s, parent_name: model.to_s, id: id)
50
+ associated_field_set.merge!(generate_associated_field(id, reflection.foreign_key))
51
+ end
52
+
53
+ def handle_unique_config_type(reflection)
54
+ klass = reflection.klass
55
+ report(:unique_association, name: klass.to_s, parent_name: @model.to_s)
56
+
57
+ id = Model::IdGenerator.new(klass).unique_id_for(@model)
58
+ associated_field_set.merge!(generate_associated_field(id, reflection.foreign_key))
59
+ end
60
+
61
+ def handle_new_config_type(reflection)
62
+ klass = reflection.klass
63
+ report(:belongs_to_associations, name: klass.to_s, parent_name: model.to_s)
64
+
65
+ new_associated_record = generate_association(klass, {}, INDEX)
66
+ associated_field_set.merge!(generate_associated_field(new_associated_record.id, reflection.foreign_key))
67
+ end
68
+
69
+ def handle_other_config_type(reflection, association_config)
70
+ klass = reflection.klass
71
+
72
+ report(:belongs_to_associations, name: klass.to_s, parent_name: model.to_s)
73
+
74
+ new_associated_record = generate_association(klass, association_config, INDEX)
75
+ associated_field_set.merge!(generate_associated_field(new_associated_record.id, reflection.foreign_key))
76
+ end
77
+
78
+ def get_type(association_config)
79
+ if association_config.is_a?(String)
80
+ raise InvalidAssociationConfigError, "Invalid association config" unless ["random", "new", "unique"].include?(association_config)
81
+
82
+ return association_config
83
+ else
84
+ association_config
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,24 @@
1
+ module Seedie
2
+ module Associations
3
+ class HasMany < BaseAssociation
4
+ def generate_associations
5
+ return if association_config["has_many"].nil?
6
+
7
+ report(:has_many_start)
8
+ association_config["has_many"].each do |association_name, association_config|
9
+ association_class = association_name.to_s.classify.constantize
10
+ count = get_association_count(association_config)
11
+ config = only_count_given?(association_config) ? {} : association_config
12
+
13
+ report(:associated_records, name: association_name, count: count, parent_name: model.to_s)
14
+ count.times do |index|
15
+ field_values_set = FieldValuesSet.new(association_class, config, index).generate_field_values
16
+ record_creator = Model::Creator.new(record.send(association_name), reporters)
17
+
18
+ record_creator.create!(field_values_set)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ module Seedie
2
+ module Associations
3
+ class HasOne < BaseAssociation
4
+ def generate_associations
5
+ return if association_config["has_one"].nil?
6
+
7
+ report(:has_one_start)
8
+
9
+ association_config["has_one"].each do |association_name, association_config|
10
+ reflection = model.reflect_on_association(association_name)
11
+ association_class = reflection.klass
12
+ count = get_association_count(association_config)
13
+
14
+ report(:associated_records, count: count, name: association_name, parent_name: model.to_s)
15
+ if count > 1
16
+ raise InvalidAssociationConfigError, "has_one association cannot be more than 1"
17
+ else
18
+ config = only_count_given?(association_config) ? {} : association_config
19
+ field_values_set = FieldValuesSet.new(association_class, config, INDEX).generate_field_values
20
+ parent_field_set = generate_associated_field(record.id, reflection.foreign_key)
21
+
22
+ record_creator = Model::Creator.new(association_class, reporters)
23
+ record_creator.create!(field_values_set.merge!(parent_field_set))
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,97 @@
1
+ module Seedie
2
+ module FieldValues
3
+ class CustomValue
4
+ VALID_KEYS = ["values", "pick_strategy"]
5
+ CUSTOM_VALUE = "custom_attr_value"
6
+
7
+ attr_reader :name, :parsed_value
8
+
9
+ def initialize(name, value_template, index)
10
+ @name = name
11
+ @value_template = value_template
12
+ @index = index
13
+ @parsed_value = ""
14
+ @custom_attr_value = false
15
+
16
+ validate_template if @value_template.is_a?(Hash) && @value_template.has_key?(CUSTOM_VALUE)
17
+ end
18
+
19
+ def generate_custom_field_value
20
+ if @value_template.is_a?(String)
21
+ generate_custom_value_from_string
22
+ else
23
+ generate_custom_value_from_hash
24
+ end
25
+
26
+ parsed_value
27
+ end
28
+
29
+ private
30
+
31
+ def validate_template
32
+ @value_template = @value_template[CUSTOM_VALUE]
33
+ @custom_attr_value = true
34
+
35
+ validate_hash_keys
36
+ validate_values_key
37
+ validate_pick_strategy_key
38
+ end
39
+
40
+ def validate_hash_keys
41
+ invalid_keys = @value_template.keys - VALID_KEYS
42
+ return if invalid_keys.empty?
43
+
44
+ raise InvalidCustomFieldKeysError,
45
+ "Invalid keys for #{@name}: #{invalid_keys.join(", ")}. Only 'values' and 'pick_strategy' are allowed."
46
+ end
47
+
48
+ def validate_values_key
49
+ return if @value_template["values"].is_a?(Array)
50
+
51
+ raise InvalidCustomFieldValuesError, "The values key for #{@name} must be an array."
52
+ end
53
+
54
+ def validate_values_length
55
+ return if @value_template["values"].length >= @index
56
+
57
+ raise CustomFieldNotEnoughValuesError, "There are not enough values for name. Please add more values."
58
+ end
59
+
60
+ def validate_pick_strategy_key
61
+ @pick_strategy = @value_template["pick_strategy"] || "random"
62
+ return if %w[random sequential].include?(@pick_strategy)
63
+
64
+ raise CustomFieldInvalidPickValueError, "The pick_strategy for #{@name} must be either 'sequential' or 'random'."
65
+ end
66
+
67
+ def generate_custom_value_from_string
68
+ @parsed_value = @value_template.gsub("{{index}}", @index.to_s)
69
+
70
+ @parsed_value.gsub!(/\{\{(.+?)\}\}/) do
71
+ method_string = $1
72
+
73
+ if method_string.start_with?("Faker::")
74
+ eval($1)
75
+ else
76
+ raise InvalidFakerMethodError, "Invalid method: #{method_string}"
77
+ end
78
+ end
79
+ end
80
+
81
+ def generate_custom_value_from_hash
82
+ if @custom_attr_value
83
+ values = @value_template["values"]
84
+ if @pick_strategy == "sequential"
85
+ validate_values_length
86
+
87
+ @parsed_value = values[@index]
88
+ else
89
+ @parsed_value = values.sample
90
+ end
91
+ else
92
+ @parsed_value = @value_template
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end