type_balancer_rails 0.2.0 → 0.2.3

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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +8 -23
  4. data/CHANGELOG.md +14 -2
  5. data/README.md +15 -0
  6. data/example/.dockerignore +51 -0
  7. data/example/.rspec +1 -0
  8. data/example/.rubocop.yml +132 -0
  9. data/example/Dockerfile +72 -0
  10. data/example/Gemfile +77 -0
  11. data/example/Gemfile.lock +449 -0
  12. data/example/README.md +42 -0
  13. data/example/Rakefile +6 -0
  14. data/example/app/assets/images/.keep +0 -0
  15. data/example/app/assets/stylesheets/application.css +10 -0
  16. data/example/app/controllers/application_controller.rb +4 -0
  17. data/example/app/controllers/concerns/.keep +0 -0
  18. data/example/app/controllers/contents_controller.rb +11 -0
  19. data/example/app/controllers/posts_controller.rb +5 -0
  20. data/example/app/helpers/application_helper.rb +2 -0
  21. data/example/app/helpers/contents_helper.rb +2 -0
  22. data/example/app/helpers/posts_helper.rb +2 -0
  23. data/example/app/jobs/application_job.rb +7 -0
  24. data/example/app/mailers/application_mailer.rb +4 -0
  25. data/example/app/models/application_record.rb +3 -0
  26. data/example/app/models/concerns/.keep +0 -0
  27. data/example/app/models/content.rb +3 -0
  28. data/example/app/models/post.rb +3 -0
  29. data/example/app/views/contents/balance_by_category.html.erb +27 -0
  30. data/example/app/views/contents/balance_by_content_type.html.erb +2 -0
  31. data/example/app/views/layouts/application.html.erb +27 -0
  32. data/example/app/views/layouts/mailer.html.erb +13 -0
  33. data/example/app/views/layouts/mailer.text.erb +1 -0
  34. data/example/app/views/posts/balance_by_category.html.erb +2 -0
  35. data/example/app/views/posts/index.html.erb +25 -0
  36. data/example/app/views/pwa/manifest.json.erb +22 -0
  37. data/example/app/views/pwa/service-worker.js +26 -0
  38. data/example/bin/brakeman +7 -0
  39. data/example/bin/dev +2 -0
  40. data/example/bin/docker-entrypoint +14 -0
  41. data/example/bin/rails +4 -0
  42. data/example/bin/rake +4 -0
  43. data/example/bin/rubocop +8 -0
  44. data/example/bin/setup +34 -0
  45. data/example/bin/thrust +5 -0
  46. data/example/config/application.rb +42 -0
  47. data/example/config/boot.rb +4 -0
  48. data/example/config/cable.yml +10 -0
  49. data/example/config/credentials.yml.enc +1 -0
  50. data/example/config/database.yml +41 -0
  51. data/example/config/environment.rb +5 -0
  52. data/example/config/environments/development.rb +72 -0
  53. data/example/config/environments/production.rb +89 -0
  54. data/example/config/environments/test.rb +53 -0
  55. data/example/config/initializers/assets.rb +7 -0
  56. data/example/config/initializers/content_security_policy.rb +25 -0
  57. data/example/config/initializers/filter_parameter_logging.rb +8 -0
  58. data/example/config/initializers/inflections.rb +16 -0
  59. data/example/config/locales/en.yml +31 -0
  60. data/example/config/master.key +1 -0
  61. data/example/config/puma.rb +41 -0
  62. data/example/config/routes.rb +8 -0
  63. data/example/config/storage.yml +34 -0
  64. data/example/config.ru +6 -0
  65. data/example/db/migrate/20250427174733_create_posts.rb +10 -0
  66. data/example/db/migrate/20250427174747_create_contents.rb +11 -0
  67. data/example/db/schema.rb +28 -0
  68. data/example/db/seeds.rb +9 -0
  69. data/example/lib/tasks/.keep +0 -0
  70. data/example/lib/tasks/dev_fixtures.rake +12 -0
  71. data/example/script/.keep +0 -0
  72. data/example/spec/controllers/contents_controller_spec.rb +27 -0
  73. data/example/spec/controllers/posts_controller_spec.rb +16 -0
  74. data/example/spec/features/contents_balancing_spec.rb +18 -0
  75. data/example/spec/features/posts_balancing_spec.rb +16 -0
  76. data/example/spec/models/content_spec.rb +19 -0
  77. data/example/spec/models/post_spec.rb +12 -0
  78. data/example/spec/rails_helper.rb +35 -0
  79. data/example/spec/spec_helper.rb +94 -0
  80. data/example/storage/.keep +0 -0
  81. data/example/storage/development.sqlite3 +0 -0
  82. data/example/storage/test.sqlite3 +0 -0
  83. data/example/test/fixtures/contents.yml +19 -0
  84. data/example/test/fixtures/posts.yml +16 -0
  85. data/example/vendor/.keep +0 -0
  86. data/lib/type_balancer/rails/collection_methods.rb +58 -33
  87. data/lib/type_balancer/rails/version.rb +1 -1
  88. data/type_balancer_rails.gemspec +1 -1
  89. metadata +85 -4
@@ -0,0 +1,10 @@
1
+ class CreatePosts < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :posts do |t|
4
+ t.string :title
5
+ t.string :media_type
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ class CreateContents < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :contents do |t|
4
+ t.string :title
5
+ t.string :category
6
+ t.string :content_type
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema[8.0].define(version: 2025_04_27_174747) do
14
+ create_table "contents", force: :cascade do |t|
15
+ t.string "title"
16
+ t.string "category"
17
+ t.string "content_type"
18
+ t.datetime "created_at", null: false
19
+ t.datetime "updated_at", null: false
20
+ end
21
+
22
+ create_table "posts", force: :cascade do |t|
23
+ t.string "title"
24
+ t.string "media_type"
25
+ t.datetime "created_at", null: false
26
+ t.datetime "updated_at", null: false
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ # This file should ensure the existence of records required to run the application in every environment (production,
2
+ # development, test). The code here should be idempotent so that it can be executed at any point in every environment.
3
+ # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
4
+ #
5
+ # Example:
6
+ #
7
+ # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
8
+ # MovieGenre.find_or_create_by!(name: genre_name)
9
+ # end
File without changes
@@ -0,0 +1,12 @@
1
+ namespace :dev do
2
+ desc "Load test fixtures into the development database (WARNING: this will delete all existing records in posts and contents!)"
3
+ task load_fixtures: :environment do
4
+ require "active_record/fixtures"
5
+ puts "Deleting all Posts and Contents..."
6
+ Post.delete_all
7
+ Content.delete_all
8
+ puts "Loading fixtures from test/fixtures..."
9
+ ActiveRecord::FixtureSet.create_fixtures("test/fixtures", [ "posts", "contents" ])
10
+ puts "Fixtures loaded!"
11
+ end
12
+ end
File without changes
@@ -0,0 +1,27 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe ContentsController, type: :controller do
4
+ fixtures :contents
5
+
6
+ describe 'GET #balance_by_category' do
7
+ it 'assigns a balanced set of contents by category to @contents' do
8
+ get :balance_by_category
9
+ expect(assigns(:contents)).to be_present
10
+ original = Content.order(:id).pluck(:category)
11
+ balanced = assigns(:contents).pluck(:category)
12
+ expect(balanced).not_to eq(original)
13
+ expect(balanced.uniq.sort).to contain_exactly('blog', 'news', 'tutorial')
14
+ end
15
+ end
16
+
17
+ describe 'GET #balance_by_content_type' do
18
+ it 'assigns a balanced set of contents by content_type to @contents' do
19
+ get :balance_by_content_type
20
+ expect(assigns(:contents)).to be_present
21
+ original = Content.order(:id).pluck(:content_type)
22
+ balanced = assigns(:contents).pluck(:content_type)
23
+ expect(balanced).not_to eq(original)
24
+ expect(balanced.uniq.sort).to contain_exactly('article', 'image', 'video')
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe PostsController, type: :controller do
4
+ fixtures :posts
5
+
6
+ describe 'GET #index' do
7
+ it 'assigns a balanced set of posts to @posts' do
8
+ get :index
9
+ expect(assigns(:posts)).to be_present
10
+ original = Post.order(:id).pluck(:media_type)
11
+ balanced = assigns(:posts).pluck(:media_type)
12
+ expect(balanced).not_to eq(original)
13
+ expect(balanced.uniq.sort).to contain_exactly('article', 'image', 'video')
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.feature 'Contents balancing', type: :feature do
4
+ fixtures :contents
5
+
6
+ scenario 'visiting the contents balance by category page displays contents balanced by category' do
7
+ visit '/contents/balance_by_category'
8
+ expect(page).to have_content('Contents (Balanced by :category)')
9
+ # Collect the categories from the second column of the table rows
10
+ categories = page.all('table tbody tr').map { |row| row.all('td')[1]&.text }.compact
11
+
12
+ expect(categories.count).to eq(Content.count)
13
+ # There should be a mix of categories, not just a long run of one category
14
+ expect(categories.uniq.sort).to eq(%w[blog news tutorial])
15
+ # Check that the first 10 are not all the same (skewed fixture would be all news)
16
+ expect(categories.first(10).uniq.size).to be > 1
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.feature 'Posts balancing', type: :feature do
4
+ fixtures :posts
5
+
6
+ scenario 'visiting the posts index displays posts balanced by media_type' do
7
+ visit '/posts'
8
+ expect(page).to have_content('Posts (Balanced by :media_type)')
9
+ # Collect the media types from the second column of the table rows
10
+ media_types = page.all('table tbody tr').map { |row| row.all('td')[1]&.text }.compact
11
+ # There should be a mix of media types, not just a long run of one type
12
+ expect(media_types.uniq.sort).to eq(%w[article image video])
13
+ # Check that the first 10 are not all the same (skewed fixture would be all video)
14
+ expect(media_types.first(10).uniq.size).to be > 1
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe Content, type: :model do
4
+ fixtures :contents
5
+
6
+ it 'balances contents by category' do
7
+ original = described_class.order(:id).pluck(:category)
8
+ balanced = described_class.all.balance_by_type(type_field: :category).pluck(:category)
9
+ expect(balanced).not_to eq(original)
10
+ expect(balanced.uniq.sort).to contain_exactly('blog', 'news', 'tutorial')
11
+ end
12
+
13
+ it 'balances contents by content_type' do
14
+ original = described_class.order(:id).pluck(:content_type)
15
+ balanced = described_class.all.balance_by_type(type_field: :content_type).pluck(:content_type)
16
+ expect(balanced).not_to eq(original)
17
+ expect(balanced.uniq.sort).to contain_exactly('article', 'image', 'video')
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe Post, type: :model do
4
+ fixtures :posts
5
+
6
+ it 'balances posts by media_type' do
7
+ original = described_class.order(:id).pluck(:media_type)
8
+ balanced = described_class.all.balance_by_type(type_field: :media_type).pluck(:media_type)
9
+ expect(balanced).not_to eq(original)
10
+ expect(balanced.uniq.sort).to contain_exactly('article', 'image', 'video')
11
+ end
12
+ end
@@ -0,0 +1,35 @@
1
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
2
+ require 'spec_helper'
3
+ ENV['RAILS_ENV'] ||= 'test'
4
+ require_relative '../config/environment'
5
+ # Prevent database truncation if the environment is production
6
+ abort("The Rails environment is running in production mode!") if Rails.env.production?
7
+ require 'rspec/rails'
8
+ require 'capybara/rails'
9
+
10
+ # Requires supporting ruby files with custom matchers and macros, etc, in
11
+ # spec/support/ and its subdirectories.
12
+ # Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
13
+
14
+ # Checks for pending migrations and applies them before tests are run.
15
+ # If you are not using ActiveRecord, you can remove these lines.
16
+ begin
17
+ ActiveRecord::Migration.maintain_test_schema!
18
+ rescue ActiveRecord::PendingMigrationError => e
19
+ abort e.to_s.strip
20
+ end
21
+
22
+ RSpec.configure do |config|
23
+ # Use test/fixtures for Rails-style fixtures
24
+ config.fixture_paths = [ "#{::Rails.root}/test/fixtures" ]
25
+ config.use_transactional_fixtures = true
26
+ config.include ActiveRecord::TestFixtures
27
+
28
+ # You can uncomment this line to turn off ActiveRecord support entirely.
29
+ # config.use_active_record = false
30
+
31
+ # Filter lines from Rails gems in backtraces.
32
+ config.filter_rails_from_backtrace!
33
+ # arbitrary gems may also be filtered via:
34
+ # config.filter_gems_from_backtrace("gem name")
35
+ end
@@ -0,0 +1,94 @@
1
+ # This file was generated by the `rails generate rspec:install` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16
+ RSpec.configure do |config|
17
+ # rspec-expectations config goes here. You can use an alternate
18
+ # assertion/expectation library such as wrong or the stdlib/minitest
19
+ # assertions if you prefer.
20
+ config.expect_with :rspec do |expectations|
21
+ # This option will default to `true` in RSpec 4. It makes the `description`
22
+ # and `failure_message` of custom matchers include text for helper methods
23
+ # defined using `chain`, e.g.:
24
+ # be_bigger_than(2).and_smaller_than(4).description
25
+ # # => "be bigger than 2 and smaller than 4"
26
+ # ...rather than:
27
+ # # => "be bigger than 2"
28
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
29
+ end
30
+
31
+ # rspec-mocks config goes here. You can use an alternate test double
32
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
33
+ config.mock_with :rspec do |mocks|
34
+ # Prevents you from mocking or stubbing a method that does not exist on
35
+ # a real object. This is generally recommended, and will default to
36
+ # `true` in RSpec 4.
37
+ mocks.verify_partial_doubles = true
38
+ end
39
+
40
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
41
+ # have no way to turn it off -- the option exists only for backwards
42
+ # compatibility in RSpec 3). It causes shared context metadata to be
43
+ # inherited by the metadata hash of host groups and examples, rather than
44
+ # triggering implicit auto-inclusion in groups with matching metadata.
45
+ config.shared_context_metadata_behavior = :apply_to_host_groups
46
+
47
+ # The settings below are suggested to provide a good initial experience
48
+ # with RSpec, but feel free to customize to your heart's content.
49
+ =begin
50
+ # This allows you to limit a spec run to individual examples or groups
51
+ # you care about by tagging them with `:focus` metadata. When nothing
52
+ # is tagged with `:focus`, all examples get run. RSpec also provides
53
+ # aliases for `it`, `describe`, and `context` that include `:focus`
54
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
55
+ config.filter_run_when_matching :focus
56
+
57
+ # Allows RSpec to persist some state between runs in order to support
58
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
59
+ # you configure your source control system to ignore this file.
60
+ config.example_status_persistence_file_path = "spec/examples.txt"
61
+
62
+ # Limits the available syntax to the non-monkey patched syntax that is
63
+ # recommended. For more details, see:
64
+ # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
65
+ config.disable_monkey_patching!
66
+
67
+ # Many RSpec users commonly either run the entire suite or an individual
68
+ # file, and it's useful to allow more verbose output when running an
69
+ # individual spec file.
70
+ if config.files_to_run.one?
71
+ # Use the documentation formatter for detailed output,
72
+ # unless a formatter has already been configured
73
+ # (e.g. via a command-line flag).
74
+ config.default_formatter = "doc"
75
+ end
76
+
77
+ # Print the 10 slowest examples and example groups at the
78
+ # end of the spec run, to help surface which specs are running
79
+ # particularly slow.
80
+ config.profile_examples = 10
81
+
82
+ # Run specs in random order to surface order dependencies. If you find an
83
+ # order dependency and want to debug it, you can fix the order by providing
84
+ # the seed, which is printed after each run.
85
+ # --seed 1234
86
+ config.order = :random
87
+
88
+ # Seed global randomization in this process using the `--seed` CLI option.
89
+ # Setting this allows you to use `--seed` to deterministically reproduce
90
+ # test failures related to randomization by passing the same `--seed` value
91
+ # as the one that triggered the failure.
92
+ Kernel.srand config.seed
93
+ =end
94
+ end
File without changes
Binary file
Binary file
@@ -0,0 +1,19 @@
1
+ # Skewed distribution for demoing balancing on two fields
2
+ <% 1.upto(30) do |i| %>
3
+ news_video_<%= i %>:
4
+ title: "News Video <%= i %>"
5
+ category: news
6
+ content_type: video
7
+ <% end %>
8
+ <% 1.upto(15) do |i| %>
9
+ blog_article_<%= i %>:
10
+ title: "Blog Article <%= i %>"
11
+ category: blog
12
+ content_type: article
13
+ <% end %>
14
+ <% 1.upto(5) do |i| %>
15
+ tutorial_image_<%= i %>:
16
+ title: "Tutorial Image <%= i %>"
17
+ category: tutorial
18
+ content_type: image
19
+ <% end %>
@@ -0,0 +1,16 @@
1
+ # Skewed distribution for demoing balancing
2
+ <% 1.upto(35) do |i| %>
3
+ video_post_<%= i %>:
4
+ title: "Video Post <%= i %>"
5
+ media_type: "video"
6
+ <% end %>
7
+ <% 1.upto(10) do |i| %>
8
+ article_post_<%= i %>:
9
+ title: "Article Post <%= i %>"
10
+ media_type: "article"
11
+ <% end %>
12
+ <% 1.upto(5) do |i| %>
13
+ image_post_<%= i %>:
14
+ title: "Image Post <%= i %>"
15
+ media_type: "image"
16
+ <% end %>
File without changes
@@ -1,37 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # lib/type_balancer/rails/collection_methods.rb
3
4
  module TypeBalancer
4
5
  module Rails
5
6
  # Provides collection methods for balancing by type
6
7
  # These methods are extended onto ActiveRecord::Relation
7
8
  module CollectionMethods
8
- # Public: Balance the collection by type
9
- #
10
- # options - Hash of options to merge with model's type_balancer_options (default: {})
11
- # :type_field - Field to balance by (default: :type)
12
- # :page - Page number for pagination (default: 1)
13
- # :per_page - Number of items per page (default: 20)
14
- #
15
- # Examples
16
- #
17
- # Post.all.balance_by_type
18
- # Post.where(published: true).balance_by_type(type_field: :category)
19
- # Post.all.balance_by_type(page: 2, per_page: 20)
20
- #
21
- # Returns ActiveRecord::Relation with balanced ordering
22
9
  def balance_by_type(options = {})
23
10
  records = to_a
24
11
  return empty_relation if records.empty?
25
12
 
26
- type_field = fetch_type_field(options)
27
- # Map to array of hashes with only id and type
28
- id_and_type_hashes = records.map do |record|
29
- { id: record.id, type: record.send(type_field) }
30
- end
31
- balanced = TypeBalancer.balance(id_and_type_hashes, type_field: :type)
13
+ type_field = fetch_type_field(options).to_sym
14
+ type_counts = records.group_by { |r| r.send(type_field).to_s }.transform_values(&:count)
15
+ type_order = compute_type_order(type_counts)
16
+ items = build_items(records, type_field)
17
+
18
+ balanced = TypeBalancer.balance(
19
+ items,
20
+ type_field: type_field,
21
+ type_order: type_order
22
+ )
23
+
32
24
  return empty_relation if balanced.nil?
33
25
 
34
26
  paged = apply_pagination(balanced, options)
27
+
35
28
  build_result(paged)
36
29
  end
37
30
 
@@ -40,33 +33,65 @@ module TypeBalancer
40
33
  def apply_pagination(records, options)
41
34
  return records unless options[:page] || options[:per_page]
42
35
 
43
- page = (options[:page] || 1).to_i
44
- per_page = (options[:per_page] || 20).to_i
45
- offset = (page - 1) * per_page
36
+ page = (options[:page] || 1).to_i
37
+ per_page = (options[:per_page] || 20).to_i
38
+ offset = (page - 1) * per_page
46
39
  records[offset, per_page] || []
47
40
  end
48
41
 
49
42
  def build_result(balanced)
50
- # Flatten in case balanced is a nested array
51
- ids = balanced.flatten.map { |h| h[:id] }
52
- # Map back to original records (works for both AR and TestRelation)
53
- records_by_id = to_a.index_by(&:id)
54
- ordered = ids.map { |id| records_by_id[id] }
55
- self.class.new(ordered)
43
+ flattened = balanced.flatten(1)
44
+ ids = flattened.map { |h| h[:id] }
45
+ unless klass.respond_to?(:where)
46
+ raise TypeError, 'balance_by_type can only be called on an ActiveRecord::Relation or compatible object'
47
+ end
48
+
49
+ relation = klass.where(id: ids)
50
+ if ids.any?
51
+ case_sql = "CASE id #{ids.each_with_index.map { |id, idx| "WHEN #{id} THEN #{idx}" }.join(' ')} END"
52
+ relation = relation.order(Arel.sql(case_sql))
53
+ end
54
+ relation
56
55
  end
57
56
 
58
57
  def empty_relation
59
- if klass.respond_to?(:none)
60
- klass.none
61
- else
62
- []
58
+ unless klass.respond_to?(:none)
59
+ raise TypeError, 'balance_by_type can only be called on an ActiveRecord::Relation or compatible object'
63
60
  end
61
+
62
+ klass.none
64
63
  end
65
64
 
66
65
  def fetch_type_field(options)
67
66
  model_opts = klass.respond_to?(:type_balancer_options) ? klass.type_balancer_options : {}
68
67
  options[:type_field] || model_opts[:type_field] || :type
69
68
  end
69
+
70
+ def compute_type_order(type_counts)
71
+ type_counts.sort_by { |_, count| count }.map(&:first)
72
+ end
73
+
74
+ def build_items(records, type_field)
75
+ records.map do |record|
76
+ { id: record.id, type_field => record.send(type_field).to_s }
77
+ end
78
+ end
79
+
80
+ def logger?
81
+ defined?(::Rails) && ::Rails.logger
82
+ end
83
+
84
+ def balance_results_lines(balanced, _type_field)
85
+ if balanced.nil?
86
+ ['Balanced result is nil!']
87
+ else
88
+ [
89
+ "First 10 balanced types: \#{balanced.first(10).map { |h| h[type_field] }.inspect}",
90
+ "Unique types in first 10: \#{balanced.first(10).map { |h| h[type_field] }.uniq.inspect}",
91
+ "Total balanced items: \#{balanced.size}"
92
+ ]
93
+ end
94
+ end
70
95
  end
71
96
  end
72
97
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TypeBalancer
4
4
  module Rails
5
- VERSION = '0.2.0'
5
+ VERSION = '0.2.3'
6
6
  end
7
7
  end
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
  # Runtime dependencies
33
33
  spec.add_dependency 'activerecord', '>= 7.0', '< 9.0'
34
34
  spec.add_dependency 'activesupport', '>= 7.0', '< 9.0'
35
- spec.add_dependency 'type_balancer', '~> 0.1', '>= 0.1.0'
35
+ spec.add_dependency 'type_balancer', '~> 0.1', '>= 0.1.4'
36
36
 
37
37
  # For more information and examples about making a new gem, check out our
38
38
  # guide at: https://bundler.io/guides/creating_gem.html
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: type_balancer_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Smith
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-27 00:00:00.000000000 Z
11
+ date: 2025-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -59,7 +59,7 @@ dependencies:
59
59
  version: '0.1'
60
60
  - - ">="
61
61
  - !ruby/object:Gem::Version
62
- version: 0.1.0
62
+ version: 0.1.4
63
63
  type: :runtime
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
@@ -69,7 +69,7 @@ dependencies:
69
69
  version: '0.1'
70
70
  - - ">="
71
71
  - !ruby/object:Gem::Version
72
- version: 0.1.0
72
+ version: 0.1.4
73
73
  description: Provides Rails integration for the type_balancer gem
74
74
  email:
75
75
  - carl@llwebconsulting.com
@@ -77,12 +77,93 @@ executables: []
77
77
  extensions: []
78
78
  extra_rdoc_files: []
79
79
  files:
80
+ - ".rspec"
80
81
  - ".rubocop.yml"
81
82
  - CHANGELOG.md
82
83
  - CODE_OF_CONDUCT.md
83
84
  - LICENSE.txt
84
85
  - README.md
85
86
  - Rakefile
87
+ - example/.dockerignore
88
+ - example/.rspec
89
+ - example/.rubocop.yml
90
+ - example/Dockerfile
91
+ - example/Gemfile
92
+ - example/Gemfile.lock
93
+ - example/README.md
94
+ - example/Rakefile
95
+ - example/app/assets/images/.keep
96
+ - example/app/assets/stylesheets/application.css
97
+ - example/app/controllers/application_controller.rb
98
+ - example/app/controllers/concerns/.keep
99
+ - example/app/controllers/contents_controller.rb
100
+ - example/app/controllers/posts_controller.rb
101
+ - example/app/helpers/application_helper.rb
102
+ - example/app/helpers/contents_helper.rb
103
+ - example/app/helpers/posts_helper.rb
104
+ - example/app/jobs/application_job.rb
105
+ - example/app/mailers/application_mailer.rb
106
+ - example/app/models/application_record.rb
107
+ - example/app/models/concerns/.keep
108
+ - example/app/models/content.rb
109
+ - example/app/models/post.rb
110
+ - example/app/views/contents/balance_by_category.html.erb
111
+ - example/app/views/contents/balance_by_content_type.html.erb
112
+ - example/app/views/layouts/application.html.erb
113
+ - example/app/views/layouts/mailer.html.erb
114
+ - example/app/views/layouts/mailer.text.erb
115
+ - example/app/views/posts/balance_by_category.html.erb
116
+ - example/app/views/posts/index.html.erb
117
+ - example/app/views/pwa/manifest.json.erb
118
+ - example/app/views/pwa/service-worker.js
119
+ - example/bin/brakeman
120
+ - example/bin/dev
121
+ - example/bin/docker-entrypoint
122
+ - example/bin/rails
123
+ - example/bin/rake
124
+ - example/bin/rubocop
125
+ - example/bin/setup
126
+ - example/bin/thrust
127
+ - example/config.ru
128
+ - example/config/application.rb
129
+ - example/config/boot.rb
130
+ - example/config/cable.yml
131
+ - example/config/credentials.yml.enc
132
+ - example/config/database.yml
133
+ - example/config/environment.rb
134
+ - example/config/environments/development.rb
135
+ - example/config/environments/production.rb
136
+ - example/config/environments/test.rb
137
+ - example/config/initializers/assets.rb
138
+ - example/config/initializers/content_security_policy.rb
139
+ - example/config/initializers/filter_parameter_logging.rb
140
+ - example/config/initializers/inflections.rb
141
+ - example/config/locales/en.yml
142
+ - example/config/master.key
143
+ - example/config/puma.rb
144
+ - example/config/routes.rb
145
+ - example/config/storage.yml
146
+ - example/db/migrate/20250427174733_create_posts.rb
147
+ - example/db/migrate/20250427174747_create_contents.rb
148
+ - example/db/schema.rb
149
+ - example/db/seeds.rb
150
+ - example/lib/tasks/.keep
151
+ - example/lib/tasks/dev_fixtures.rake
152
+ - example/script/.keep
153
+ - example/spec/controllers/contents_controller_spec.rb
154
+ - example/spec/controllers/posts_controller_spec.rb
155
+ - example/spec/features/contents_balancing_spec.rb
156
+ - example/spec/features/posts_balancing_spec.rb
157
+ - example/spec/models/content_spec.rb
158
+ - example/spec/models/post_spec.rb
159
+ - example/spec/rails_helper.rb
160
+ - example/spec/spec_helper.rb
161
+ - example/storage/.keep
162
+ - example/storage/development.sqlite3
163
+ - example/storage/test.sqlite3
164
+ - example/test/fixtures/contents.yml
165
+ - example/test/fixtures/posts.yml
166
+ - example/vendor/.keep
86
167
  - lib/generators/type_balancer/install/install_generator.rb
87
168
  - lib/generators/type_balancer/install/templates/type_balancer.rb.erb
88
169
  - lib/type_balancer/rails.rb