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.
- checksums.yaml +4 -4
- data/.rspec +4 -0
- data/.rubocop.yml +8 -23
- data/CHANGELOG.md +14 -2
- data/README.md +15 -0
- data/example/.dockerignore +51 -0
- data/example/.rspec +1 -0
- data/example/.rubocop.yml +132 -0
- data/example/Dockerfile +72 -0
- data/example/Gemfile +77 -0
- data/example/Gemfile.lock +449 -0
- data/example/README.md +42 -0
- data/example/Rakefile +6 -0
- data/example/app/assets/images/.keep +0 -0
- data/example/app/assets/stylesheets/application.css +10 -0
- data/example/app/controllers/application_controller.rb +4 -0
- data/example/app/controllers/concerns/.keep +0 -0
- data/example/app/controllers/contents_controller.rb +11 -0
- data/example/app/controllers/posts_controller.rb +5 -0
- data/example/app/helpers/application_helper.rb +2 -0
- data/example/app/helpers/contents_helper.rb +2 -0
- data/example/app/helpers/posts_helper.rb +2 -0
- data/example/app/jobs/application_job.rb +7 -0
- data/example/app/mailers/application_mailer.rb +4 -0
- data/example/app/models/application_record.rb +3 -0
- data/example/app/models/concerns/.keep +0 -0
- data/example/app/models/content.rb +3 -0
- data/example/app/models/post.rb +3 -0
- data/example/app/views/contents/balance_by_category.html.erb +27 -0
- data/example/app/views/contents/balance_by_content_type.html.erb +2 -0
- data/example/app/views/layouts/application.html.erb +27 -0
- data/example/app/views/layouts/mailer.html.erb +13 -0
- data/example/app/views/layouts/mailer.text.erb +1 -0
- data/example/app/views/posts/balance_by_category.html.erb +2 -0
- data/example/app/views/posts/index.html.erb +25 -0
- data/example/app/views/pwa/manifest.json.erb +22 -0
- data/example/app/views/pwa/service-worker.js +26 -0
- data/example/bin/brakeman +7 -0
- data/example/bin/dev +2 -0
- data/example/bin/docker-entrypoint +14 -0
- data/example/bin/rails +4 -0
- data/example/bin/rake +4 -0
- data/example/bin/rubocop +8 -0
- data/example/bin/setup +34 -0
- data/example/bin/thrust +5 -0
- data/example/config/application.rb +42 -0
- data/example/config/boot.rb +4 -0
- data/example/config/cable.yml +10 -0
- data/example/config/credentials.yml.enc +1 -0
- data/example/config/database.yml +41 -0
- data/example/config/environment.rb +5 -0
- data/example/config/environments/development.rb +72 -0
- data/example/config/environments/production.rb +89 -0
- data/example/config/environments/test.rb +53 -0
- data/example/config/initializers/assets.rb +7 -0
- data/example/config/initializers/content_security_policy.rb +25 -0
- data/example/config/initializers/filter_parameter_logging.rb +8 -0
- data/example/config/initializers/inflections.rb +16 -0
- data/example/config/locales/en.yml +31 -0
- data/example/config/master.key +1 -0
- data/example/config/puma.rb +41 -0
- data/example/config/routes.rb +8 -0
- data/example/config/storage.yml +34 -0
- data/example/config.ru +6 -0
- data/example/db/migrate/20250427174733_create_posts.rb +10 -0
- data/example/db/migrate/20250427174747_create_contents.rb +11 -0
- data/example/db/schema.rb +28 -0
- data/example/db/seeds.rb +9 -0
- data/example/lib/tasks/.keep +0 -0
- data/example/lib/tasks/dev_fixtures.rake +12 -0
- data/example/script/.keep +0 -0
- data/example/spec/controllers/contents_controller_spec.rb +27 -0
- data/example/spec/controllers/posts_controller_spec.rb +16 -0
- data/example/spec/features/contents_balancing_spec.rb +18 -0
- data/example/spec/features/posts_balancing_spec.rb +16 -0
- data/example/spec/models/content_spec.rb +19 -0
- data/example/spec/models/post_spec.rb +12 -0
- data/example/spec/rails_helper.rb +35 -0
- data/example/spec/spec_helper.rb +94 -0
- data/example/storage/.keep +0 -0
- data/example/storage/development.sqlite3 +0 -0
- data/example/storage/test.sqlite3 +0 -0
- data/example/test/fixtures/contents.yml +19 -0
- data/example/test/fixtures/posts.yml +16 -0
- data/example/vendor/.keep +0 -0
- data/lib/type_balancer/rails/collection_methods.rb +58 -33
- data/lib/type_balancer/rails/version.rb +1 -1
- data/type_balancer_rails.gemspec +1 -1
- metadata +85 -4
@@ -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
|
data/example/db/seeds.rb
ADDED
@@ -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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
balanced = TypeBalancer.balance(
|
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
|
44
|
-
per_page
|
45
|
-
offset
|
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
|
-
|
51
|
-
ids =
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
60
|
-
|
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
|
data/type_balancer_rails.gemspec
CHANGED
@@ -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.
|
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.
|
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-
|
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.
|
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.
|
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
|