rails-minify 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: 32a43a0b86ec6d79152580e59ed32f5468ddd3d1a4e0865716f404bd542b566c
4
+ data.tar.gz: fa640778b9324a8737ff1e809af7a101ff2e10b910677611de081b033678c983
5
+ SHA512:
6
+ metadata.gz: d078aaa82950d4407b91e82fcde444264496d72551db08177d629cdb61d5928ff23381462f23388cbf5db91c138cca78d1f79a870f0eeca794d178b0e8427af9
7
+ data.tar.gz: e6ee0e7f39335eaba0d2adf4718a89dbdf52e8d1a8b993539ddfa3081631714bac16c5fc899b6f8dea5fd3cf12d667875843a1d73ccc524ecc72735c9e42834d
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-10-18)
4
+
5
+ First release.
6
+
7
+ - `config.rails_minify.enabled` turns on activerecord-, activesupport- and actionpack-minimizer.
8
+ - Patches: `:action_text_plain_text` (cached plain text, including blob attachments), `:request_host` (rails/rails#59104 for Rails 8.0/8.1), `:omniauth` (strategies skip requests that can't be theirs).
9
+ - `minify:census` and `minify:check` tasks.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sampo Kuokkanen
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # rails-minify
2
+
3
+ Cuts what Rails (and common gems) build or redo on every request.
4
+
5
+ ```ruby
6
+ # Gemfile
7
+ gem "rails-minify"
8
+
9
+ # config/environments/production.rb
10
+ config.rails_minify.enabled = true
11
+ ```
12
+
13
+ `enabled` turns on every part below; each can still be set on its own
14
+ (`config.ar_minimizer.enums = false`, `config.rails_minify.patches = [:omniauth]`).
15
+ Ruby 3.4+, Rails 8.0, 8.1 and main.
16
+
17
+ ## Strict by default
18
+
19
+ Active Record defines, for every column, a reader, a writer and 19 more
20
+ methods (`name?`, `name_changed?`, `saved_change_to_name?`, ...). With
21
+ rails-minify they're gone: only readers and writers are generated, which
22
+ on a 191-model app is 42,210 → ~4,500 methods, ~10 MB less memory and ~150
23
+ ms less boot. Calling a removed method raises NoMethodError naming its
24
+ replacement:
25
+
26
+ ```
27
+ name_changed? was removed by activerecord-minimizer's strict mode; use attribute_changed?(:name, ...)
28
+ ```
29
+
30
+ rubocop-minify rewrites those calls in your code. For calls in gems
31
+ (Devise's `will_save_change_to_email?`, ...), list them in
32
+ `config/ar_minimizer_keep.txt`; `bin/rails ar_minimizer:profile` finds them.
33
+ `config.ar_minimizer.strict = false` keeps the removed methods working
34
+ instead, generated the first time each is called.
35
+
36
+ ## What's in it
37
+
38
+ | part | does | gem |
39
+ |---|---|---|
40
+ | attribute methods | readers and writers only (strict) | activerecord-minimizer |
41
+ | enums | predicates/bang methods as plain methods (1.3-1.6x) | activerecord-minimizer |
42
+ | small records | no Hashes for records that never fill them (-22% per record) | activerecord-minimizer |
43
+ | callbacks | chains as generated methods (`valid?` -33..45%) | activesupport-minimizer |
44
+ | route helpers | `post_path(post)` as interpolation (3-14x) | actionpack-minimizer |
45
+ | views | `render`/`link_to` without their machinery (2x per page of partials) | actionpack-minimizer |
46
+ | patches | below | rails-minify |
47
+ | rewrites | removed-method calls in Ruby and ERB, as RuboCop autocorrections | rubocop-minify (development) |
48
+
49
+ ## Patches
50
+
51
+ For parts of Rails and common gems that redo work on every request.
52
+ `config.rails_minify.patches = [...]` picks them; all by default.
53
+
54
+ * `:action_text_plain_text`: `ActionText::Content#to_plain_text` parses the
55
+ HTML with Nokogiri, and looks up every attachment's record, on every call.
56
+ It's computed once per distinct HTML (cache capped at 8 MB of HTML).
57
+ Attachments that are Active Storage blobs, remote images or inline content
58
+ are cached too: a blob without a caption contributes its filename, which a
59
+ hit checks in one query (renamed or deleted: computed again). Other
60
+ attachables aren't cached. In golf-buddy, 13% of all allocations on its
61
+ pages came from it, and guides with images another 4.5%.
62
+ * `:request_host`: `ActionDispatch::Request#host` runs a regexp substitution
63
+ on every call, port or not, and Rails reads it several times per request.
64
+ rails/rails#59104 (Rails 8.2) for 8.1: no substitution without a port, and
65
+ the host kept while the raw host is the same String. Not installed on 8.2.
66
+ * `:omniauth`: every OmniAuth strategy middleware dups itself and its
67
+ options on every request before looking at the path. Requests that can't
68
+ be OmniAuth's go straight to the app. Only when that's certain up front:
69
+ a session is present, not in test mode, no `other_phase` or callable
70
+ paths, and the strategy doesn't override how paths are matched.
71
+
72
+ ## Numbers
73
+
74
+ golf-buddy, 132 production pages, with everything on (strict mode after
75
+ rubocop-minify's rewrites, all patches): identical HTML, and
76
+ 1,561,283 → 1,224,141 allocations per pass (−21.6%). The machine was too
77
+ loaded for wall-clock times to mean much.
78
+
79
+ ## Checks
80
+
81
+ * `bin/rails minify:census`: how much of the app runs compiled, and why the rest doesn't.
82
+ * `bin/rails minify:check`: callbacks naming methods that don't exist (for CI).
83
+
84
+ ## Things found that are app config, not patches
85
+
86
+ * sentry-rails' `breadcrumbs_logger = [:active_support_logger]` subscribes to
87
+ every notification (each SQL query, render, instantiation) and takes
88
+ `Time.now` twice per event, Sentry enabled or not. `:monotonic_active_support_logger`
89
+ avoids the clock calls; dropping it avoids the rest.
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_minify"
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMinify
4
+ module Patches
5
+ # ActionText::Content#to_plain_text walks the whole HTML tree with
6
+ # Nokogiri on every call, and looks up the record behind every attachment:
7
+ # in golf-buddy, 13% of all allocations on its pages came from it
8
+ # (excerpts, meta descriptions, cards). The text is computed once per
9
+ # distinct HTML and kept, up to MAX_BYTES of HTML.
10
+ #
11
+ # Without attachments the text depends on the HTML alone. An attachment
12
+ # adds its record's text: for Active Storage blobs "[caption]" (from the
13
+ # HTML) or, without a caption, "[filename]". So content whose attachments
14
+ # are all blobs, remote images or inline content is cached too, with the
15
+ # filenames it used: a hit with caption-less blobs checks them in one
16
+ # query (renamed or deleted: computed again). Attachments of other
17
+ # records (an app's own attachables, whose text can be anything) aren't
18
+ # cached.
19
+ module ActionTextPlainText
20
+ MAX_BYTES = 8 * 1024 * 1024
21
+ ATTACHMENT = /<action-text-attachment|data-trix-attachment/
22
+
23
+ @cache = {} # [html, canonicalize] => text, or [text, { blob id => filename }] with attachments
24
+ @bytes = 0
25
+ @mutex = Mutex.new
26
+
27
+ class << self
28
+ def install!
29
+ ActionText::Content.prepend(Content) unless ActionText::Content < Content
30
+ end
31
+
32
+ def fetch(key)
33
+ @cache.fetch(key) { store(key, yield.freeze) }
34
+ end
35
+
36
+ # The text, from the cache when the blobs it named are unchanged.
37
+ def fetch_with_attachments(key, content)
38
+ if (entry = @cache[key]) && filenames_unchanged?(entry[1])
39
+ return entry[0]
40
+ end
41
+
42
+ text = yield.freeze
43
+ filenames = blob_filenames(content)
44
+ store(key, [text, filenames]) if filenames
45
+ text
46
+ end
47
+
48
+ def clear! = @mutex.synchronize { @cache = {}; @bytes = 0 }
49
+
50
+ private
51
+
52
+ def store(key, value)
53
+ @mutex.synchronize do
54
+ if @bytes + key[0].bytesize > MAX_BYTES
55
+ @cache = {}
56
+ @bytes = 0
57
+ end
58
+ @bytes += key[0].bytesize unless @cache.key?(key)
59
+ @cache[key] = value
60
+ end
61
+ value
62
+ end
63
+
64
+ # { blob id => filename } for the caption-less blobs, or nil when an
65
+ # attachment's text could depend on more than that.
66
+ def blob_filenames(content)
67
+ content.attachments.each_with_object({}) do |attachment, filenames|
68
+ case attachable = attachment.attachable
69
+ when ActiveStorage::Blob
70
+ filenames[attachable.id] = attachable[:filename] unless attachment.caption
71
+ when ActionText::Attachables::RemoteImage, ActionText::Attachables::ContentAttachment
72
+ # the text comes from the HTML
73
+ else
74
+ return nil
75
+ end
76
+ end
77
+ end
78
+
79
+ def filenames_unchanged?(filenames)
80
+ filenames.empty? || ActiveStorage::Blob.where(id: filenames.keys).pluck(:id, :filename).to_h == filenames
81
+ end
82
+ end
83
+
84
+ module Content
85
+ def initialize(content = nil, options = {})
86
+ super
87
+ return unless content.is_a?(String)
88
+
89
+ @plain_text_key = [content.frozen? ? content : content.dup.freeze, options.fetch(:canonicalize, true)]
90
+ @plain_text_attachments = content.match?(ATTACHMENT)
91
+ end
92
+
93
+ def to_plain_text
94
+ return super unless @plain_text_key
95
+ return ActionTextPlainText.fetch(@plain_text_key) { super }.dup unless @plain_text_attachments
96
+
97
+ ActionTextPlainText.fetch_with_attachments(@plain_text_key, self) { super }.dup
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMinify
4
+ module Patches
5
+ # Every OmniAuth strategy is a middleware that, on every request, dups
6
+ # itself (and its Hashie::Mash options) before looking at the path: in
7
+ # golf-buddy about 35k allocations per pass over its pages, none of them
8
+ # on a login path. Requests that can't be OmniAuth's go straight to the
9
+ # app; anything that might be (or that the strategy decides at runtime:
10
+ # callable paths, other_phase, overridden path methods, test mode, a
11
+ # missing session, which OmniAuth reports) runs OmniAuth as before.
12
+ module OmniAuthPassthrough
13
+ # What decides whether a request is the strategy's. A strategy that
14
+ # overrides any of these decides for itself: always run it.
15
+ PATH_METHODS = %i[call! on_auth_path? on_request_path? on_callback_path? on_path? request_path
16
+ callback_path current_path path_prefix script_name].freeze
17
+
18
+ def self.install!
19
+ OmniAuth::Strategy.prepend(self) unless OmniAuth::Strategy < self
20
+ end
21
+
22
+ # Per strategy class: are all of PATH_METHODS OmniAuth's own?
23
+ def self.standard_paths?(klass)
24
+ @standard ||= {}.compare_by_identity
25
+ @standard.fetch(klass) do
26
+ @standard[klass] = PATH_METHODS.all? { |name| [OmniAuth::Strategy, self].include?(klass.instance_method(name).owner) }
27
+ end
28
+ end
29
+
30
+ def call(env)
31
+ return super unless passthrough?(env)
32
+
33
+ @app.call(env)
34
+ end
35
+
36
+ private
37
+
38
+ def passthrough?(env)
39
+ return false if OmniAuth.config.test_mode || !env["rack.session"] || respond_to?(:other_phase)
40
+ return false unless OmniAuthPassthrough.standard_paths?(self.class)
41
+
42
+ request_path = options[:request_path]
43
+ callback_path = options[:callback_path]
44
+ return false if request_path.respond_to?(:call) || callback_path.respond_to?(:call)
45
+
46
+ path_info = env["PATH_INFO"].to_s.downcase
47
+ full_path = "#{env["SCRIPT_NAME"]}#{path_info}".downcase
48
+ prefixes = [options[:path_prefix] || OmniAuth.config.path_prefix, request_path, callback_path]
49
+ prefixes.none? { |prefix| prefix.is_a?(String) && (path_info.start_with?(prefix.downcase) || full_path.start_with?(prefix.downcase)) }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMinify
4
+ module Patches
5
+ # ActionDispatch::Request#host runs a regexp substitution on every call,
6
+ # allocating a String and a MatchData even when the host has no port
7
+ # (behind a proxy, the usual case), and Rails reads it several times per
8
+ # request: url_options, routes constrained by host, cookies, redirects.
9
+ #
10
+ # The same change as rails/rails#59104 (Rails 8.2) for earlier versions:
11
+ # no substitution without a colon, and the host kept for as long as the
12
+ # raw host is the same String (the env's HTTP_HOST; a test's host= or
13
+ # X-Forwarded-Host make a new one, so nothing goes stale).
14
+ module RequestHost
15
+ PORT = /:\d+$/
16
+
17
+ def self.install!
18
+ return if ActionPack.gem_version >= Gem::Version.new("8.2.0.alpha") # upstream already
19
+
20
+ ActionDispatch::Request.prepend(self) unless ActionDispatch::Request < self
21
+ end
22
+
23
+ def host
24
+ raw = raw_host_with_port
25
+ return @__rm_host if @__rm_host_raw.equal?(raw)
26
+
27
+ @__rm_host_raw = raw
28
+ @__rm_host = raw.include?(":") ? raw.sub(PORT, "") : raw
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMinify
4
+ class Railtie < Rails::Railtie
5
+ config.rails_minify = ActiveSupport::OrderedOptions.new
6
+
7
+ # After config/environments/*.rb, before any initializer reads the
8
+ # minimizers' options.
9
+ config.before_initialize do |app|
10
+ next unless app.config.rails_minify.enabled
11
+
12
+ ar = app.config.ar_minimizer
13
+ ar.slim = true if ar.slim.nil?
14
+ ar.views = true if ar.views.nil?
15
+ ar.enums = true if ar.enums.nil?
16
+ ar.small_records = true if ar.small_records.nil?
17
+ ar.load_time = app.root.join("config/ar_minimizer.json").exist? if ar.load_time.nil?
18
+ as = app.config.as_minimizer
19
+ as.unroll_callbacks = true if as.unroll_callbacks.nil?
20
+ ap = app.config.ap_minimizer
21
+ ap.route_helpers = true if ap.route_helpers.nil?
22
+ ap.views = true if ap.views.nil?
23
+ end
24
+
25
+ PATCHES = {
26
+ action_text_plain_text: ["ActionText::Content", :action_text_content, -> { Patches::ActionTextPlainText.install! }],
27
+ omniauth: ["OmniAuth::Strategy", nil, -> { Patches::OmniAuthPassthrough.install! }],
28
+ request_host: ["ActionDispatch::Request", nil, -> { Patches::RequestHost.install! }],
29
+ }.freeze
30
+
31
+ initializer "rails_minify.patches" do |app|
32
+ options = app.config.rails_minify
33
+ next unless options.enabled || options.patches
34
+
35
+ Array(options.patches || PATCHES.keys).each do |name|
36
+ constant, hook, install = PATCHES.fetch(name.to_sym)
37
+ if hook
38
+ ActiveSupport.on_load(hook) { install.call }
39
+ else
40
+ config.after_initialize { install.call if Object.const_defined?(constant) }
41
+ end
42
+ end
43
+ end
44
+
45
+ rake_tasks do
46
+ namespace :minify do
47
+ prepare = lambda do
48
+ app = Rails.application
49
+ app.eager_load!
50
+ # Routes define methods too (Devise's authenticate_user!); Rails 8 loads them lazily.
51
+ app.respond_to?(:reload_routes_unless_loaded) ? app.reload_routes_unless_loaded : app.reload_routes!
52
+ app
53
+ end
54
+
55
+ desc "How much of the app runs compiled, and why the rest doesn't (VERBOSE=1 for every item)"
56
+ task census: :environment do
57
+ app = prepare.call
58
+ verbose = ENV["VERBOSE"] == "1"
59
+ routed = AsMinimizer::Check.routed_actions(app.routes)
60
+ sections = {
61
+ "Active Record (activerecord-minimizer)" => ArMinimizer::Census.report(root: app.root),
62
+ "Callbacks (activesupport-minimizer)" => AsMinimizer::Census.report(routed: routed),
63
+ "Routes and views (actionpack-minimizer)" => ApMinimizer::Census.report(app.routes, app.paths["app/views"].existent),
64
+ }
65
+ sections.each do |title, result|
66
+ puts title
67
+ result.lines(verbose: verbose).each { puts " #{it}" }
68
+ puts
69
+ end
70
+ end
71
+
72
+ desc "Fail on what would break at runtime or compile stale: missing callback targets, an out-of-date manifest"
73
+ task check: :environment do
74
+ app = prepare.call
75
+ problems = AsMinimizer::Check.callback_targets(routed: AsMinimizer::Check.routed_actions(app.routes))
76
+ missing = problems.select { it.status == :missing }
77
+ missing.each { puts " #{it}" }
78
+ manifest = ArMinimizer::Census.manifest_state(app.root, ArMinimizer::Index.from_models)
79
+ puts "callback targets: #{missing.size} missing; manifest: #{manifest}"
80
+ abort if missing.any? || manifest.start_with?("out of date")
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMinify
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_minify/version"
4
+ require "as_minimizer"
5
+ require "ar_minimizer"
6
+ require "ap_minimizer"
7
+
8
+ # One switch for the minimizers:
9
+ #
10
+ # # config/environments/production.rb
11
+ # config.rails_minify.enabled = true
12
+ #
13
+ # turns on, unless set otherwise on their own:
14
+ # config.ar_minimizer.slim, .views, .load_time (load_time only with config/ar_minimizer.json)
15
+ # config.as_minimizer.unroll_callbacks
16
+ # config.ap_minimizer.route_helpers, .views
17
+ #
18
+ # and these patches, for parts of Rails and common gems that redo work on
19
+ # every request (config.rails_minify.patches = [...] to choose):
20
+ # :action_text_plain_text ActionText::Content#to_plain_text, once per distinct HTML
21
+ # :omniauth OmniAuth strategies skip their per-request dup off their paths
22
+ module RailsMinify
23
+ module Patches
24
+ autoload :ActionTextPlainText, "rails_minify/patches/action_text_plain_text"
25
+ autoload :OmniAuthPassthrough, "rails_minify/patches/omniauth_passthrough"
26
+ autoload :RequestHost, "rails_minify/patches/request_host"
27
+ end
28
+ end
29
+
30
+ require "rails_minify/railtie" if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-minify
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sampo Kuokkanen
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord-minimizer
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: actionpack-minimizer
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activesupport-minimizer
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.1'
54
+ description: Turns on activerecord-, activesupport- and actionpack-minimizer, and
55
+ patches parts of Rails and common gems that redo work on every request (Action Text
56
+ plain text, Request#host, OmniAuth strategies).
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - CHANGELOG.md
62
+ - LICENSE.txt
63
+ - README.md
64
+ - lib/rails-minify.rb
65
+ - lib/rails_minify.rb
66
+ - lib/rails_minify/patches/action_text_plain_text.rb
67
+ - lib/rails_minify/patches/omniauth_passthrough.rb
68
+ - lib/rails_minify/patches/request_host.rb
69
+ - lib/rails_minify/railtie.rb
70
+ - lib/rails_minify/version.rb
71
+ homepage: https://github.com/sampokuokkanen/rails-minify/tree/main/rails-minify
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ source_code_uri: https://github.com/sampokuokkanen/rails-minify/tree/main/rails-minify
76
+ changelog_uri: https://github.com/sampokuokkanen/rails-minify/blob/main/rails-minify/CHANGELOG.md
77
+ bug_tracker_uri: https://github.com/sampokuokkanen/rails-minify/issues
78
+ rubygems_mfa_required: 'true'
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '3.4'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubygems_version: 4.0.20
94
+ specification_version: 4
95
+ summary: 'Precompile what Rails works out at runtime: attribute methods, callback
96
+ chains'
97
+ test_files: []