seedie 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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