activerecord-minimizer 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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +143 -0
- data/lib/activerecord-minimizer.rb +3 -0
- data/lib/ar_minimizer/build.rb +54 -0
- data/lib/ar_minimizer/census.rb +52 -0
- data/lib/ar_minimizer/enums.rb +61 -0
- data/lib/ar_minimizer/index.rb +218 -0
- data/lib/ar_minimizer/loader.rb +43 -0
- data/lib/ar_minimizer/manifest.rb +65 -0
- data/lib/ar_minimizer/profile.rb +62 -0
- data/lib/ar_minimizer/railtie.rb +111 -0
- data/lib/ar_minimizer/rewriter.rb +470 -0
- data/lib/ar_minimizer/slim.rb +258 -0
- data/lib/ar_minimizer/small_records.rb +97 -0
- data/lib/ar_minimizer/version.rb +5 -0
- data/lib/ar_minimizer/views.rb +50 -0
- data/lib/ar_minimizer.rb +34 -0
- metadata +112 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArMinimizer
|
|
4
|
+
# Prepended to ActiveRecord::Base's singleton class. Skips generating the
|
|
5
|
+
# attribute methods whose proxy is not in +keep+ (by default everything but
|
|
6
|
+
# readers and writers).
|
|
7
|
+
#
|
|
8
|
+
# Strict (the default): those methods are gone. Calling one raises
|
|
9
|
+
# NoMethodError naming its replacement; rubocop-minify rewrites the calls,
|
|
10
|
+
# keep_methods lists what gems call by name.
|
|
11
|
+
#
|
|
12
|
+
# strict: false: they keep working through ActiveModel's method_missing,
|
|
13
|
+
# and with +lazy+ the first call generates that one method on that model.
|
|
14
|
+
module Slim
|
|
15
|
+
# Proxy targets whose methods are still generated: the reader and the writer.
|
|
16
|
+
DEFAULT_KEEP = %w[attribute attribute=].freeze
|
|
17
|
+
|
|
18
|
+
MATCH = ActiveModel::AttributeMethods::ClassMethods::AttributeMethodPattern::AttributeMethod
|
|
19
|
+
|
|
20
|
+
# Rails asks respond_to? before calling these, and quietly does something
|
|
21
|
+
# else when the answer is no: form fields show the cast value instead of
|
|
22
|
+
# what the user typed, numericality validates the cast value ("abc" -> 0).
|
|
23
|
+
# Strict mode keeps them answering, generated on first call.
|
|
24
|
+
PROBED = { "_before_type_cast" => "attribute_before_type_cast", "_came_from_user?" => "attribute_came_from_user?" }.freeze
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
attr_reader :keep, :keep_methods
|
|
28
|
+
|
|
29
|
+
def keep=(proxies)
|
|
30
|
+
@keep = proxies.map(&:to_s).to_set.freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Method names ("name_changed?") or "Model#name_changed?" to keep
|
|
34
|
+
# generating: calls in gems or views that would otherwise take the slow
|
|
35
|
+
# path on a hot path. ArMinimizer::Profile lists them.
|
|
36
|
+
def keep_methods=(names)
|
|
37
|
+
@keep_methods = names.map { it.to_s.strip }.reject(&:empty?).to_set.freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# One "name" or "Model#name" per line; " # ..." starts a comment.
|
|
41
|
+
def parse_keep_file(text)
|
|
42
|
+
text.lines.map { it.sub(/(\A|\s)#.*/, "").strip }.reject(&:empty?)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def enabled? = @enabled == true
|
|
46
|
+
|
|
47
|
+
def lazy? = @lazy == true
|
|
48
|
+
|
|
49
|
+
def strict? = @strict == true
|
|
50
|
+
|
|
51
|
+
# Call before any model instantiates (an initializer is fine).
|
|
52
|
+
#
|
|
53
|
+
# strict: the skipped methods are gone, not generated on first call:
|
|
54
|
+
# calling one raises NoMethodError naming its replacement
|
|
55
|
+
# (attribute_changed?(:name) for name_changed?), respond_to? says no,
|
|
56
|
+
# and records answer respond_to?/method_missing for unknown names
|
|
57
|
+
# without ActiveModel's scan over every attribute method pattern.
|
|
58
|
+
# rubocop-minify rewrites the calls.
|
|
59
|
+
def enable!(keep: DEFAULT_KEEP, keep_methods: [], lazy: true, strict: true)
|
|
60
|
+
self.keep = keep
|
|
61
|
+
self.keep_methods = keep_methods
|
|
62
|
+
@strict = strict
|
|
63
|
+
@lazy = lazy && !strict
|
|
64
|
+
@enabled = true
|
|
65
|
+
ActiveRecord::Base.singleton_class.prepend(self) unless ActiveRecord::Base.singleton_class < self
|
|
66
|
+
ActiveRecord::Base.prepend(Record) unless ActiveRecord::Base < Record
|
|
67
|
+
reset_models
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def disable!
|
|
71
|
+
@enabled = false
|
|
72
|
+
@strict = false
|
|
73
|
+
reset_models
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The class's own String for an attribute name (what generated methods
|
|
77
|
+
# pass), so records don't keep copies made by method-name matching.
|
|
78
|
+
def canonical_name(klass, name)
|
|
79
|
+
names = klass.instance_variable_get(:@__arm_attribute_names) ||
|
|
80
|
+
klass.instance_variable_set(:@__arm_attribute_names, klass.attribute_names.to_h { [it, it] }.freeze)
|
|
81
|
+
names[name] || name
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def skip?(model, pattern, attr_name, name)
|
|
85
|
+
return false unless enabled?
|
|
86
|
+
return false if Thread.current[:ar_minimizer_generating] == name
|
|
87
|
+
return false if keep.include?(pattern.proxy_target)
|
|
88
|
+
return false if keep_methods.include?(name) || keep_methods.include?("#{model.name}##{name}")
|
|
89
|
+
|
|
90
|
+
!framework_calls?(model, pattern.proxy_target, attr_name.to_s)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# For the strict-mode error: the call that replaces a removed method.
|
|
94
|
+
def replacement(model, name)
|
|
95
|
+
model.attribute_method_patterns.each do |pattern|
|
|
96
|
+
next if keep.include?(pattern.proxy_target)
|
|
97
|
+
|
|
98
|
+
attr = pattern.respond_to?(:matched_attribute_name) ? pattern.matched_attribute_name(name) : pattern.match(name)&.attr_name
|
|
99
|
+
next unless attr && (model._has_attribute?(attr) || model.attribute_alias?(attr))
|
|
100
|
+
|
|
101
|
+
proxy = pattern.proxy_target == "attribute?" ? "query_attribute" : pattern.proxy_target
|
|
102
|
+
args = pattern.parameters == false ? ":#{attr}" : ":#{attr}, ..."
|
|
103
|
+
return "#{proxy}(#{args})#{" (private: on self)" unless ActiveRecord::Base.public_method_defined?(proxy)}"
|
|
104
|
+
end
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Generates the skipped method +name+ on +model+, as ActiveRecord would
|
|
109
|
+
# have. Only for the class's own attributes and aliases: columns that
|
|
110
|
+
# only some query selects (`AS trips_count`) never get methods in Rails either.
|
|
111
|
+
def generate(model, name, match)
|
|
112
|
+
return unless model._has_attribute?(match.attr_name) || model.attribute_alias?(match.attr_name)
|
|
113
|
+
|
|
114
|
+
pattern = model.attribute_method_patterns.find { it.proxy_target == match.proxy_target }
|
|
115
|
+
target = model.attribute_aliases[match.attr_name]
|
|
116
|
+
ActiveRecord::AttributeMethods::GeneratedAttributeMethods::LOCK.synchronize do
|
|
117
|
+
Thread.current[:ar_minimizer_generating] = name
|
|
118
|
+
ActiveSupport::CodeGenerator.batch(model.send(:generated_attribute_methods), __FILE__, __LINE__) do |owner|
|
|
119
|
+
if target
|
|
120
|
+
model.send(:define_attribute_method_pattern, pattern, target, owner: owner, as: match.attr_name, override: true)
|
|
121
|
+
else
|
|
122
|
+
model.send(:define_attribute_method_pattern, pattern, match.attr_name, owner: owner, as: match.attr_name)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
ensure
|
|
126
|
+
Thread.current[:ar_minimizer_generating] = nil
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
# For models loaded before the switch. ActiveRecord only undefines once
|
|
133
|
+
# attribute methods were generated, but alias_attribute defines its
|
|
134
|
+
# methods right away, in the class body.
|
|
135
|
+
def reset_models
|
|
136
|
+
ActiveRecord::Base.descendants.each do |model|
|
|
137
|
+
model.undefine_attribute_methods
|
|
138
|
+
model.send(:generated_attribute_methods).module_eval { undef_method(*instance_methods(false)) }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Methods Rails itself calls by name on hot paths.
|
|
143
|
+
def framework_calls?(model, proxy, attr)
|
|
144
|
+
case proxy
|
|
145
|
+
# every enum predicate (`published?`) and enum scope calls status_for_database
|
|
146
|
+
when "attribute_for_database" then model.defined_enums.key?(attr)
|
|
147
|
+
# cache_version, on every cache key with recyclable cache keys
|
|
148
|
+
when "attribute_before_type_cast", "attribute_came_from_user?" then attr == "updated_at"
|
|
149
|
+
else false
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc:
|
|
155
|
+
name = pattern.method_name(as)
|
|
156
|
+
# alias_attribute passes override: true (and attr_name: the aliased
|
|
157
|
+
# attribute); Record teaches method_missing to resolve those.
|
|
158
|
+
return super unless Slim.skip?(self, pattern, attr_name, name)
|
|
159
|
+
# Only real attributes: the slow path can't find anything else (gems such
|
|
160
|
+
# as activerecord-typedstore call define_attribute_method for store keys).
|
|
161
|
+
return super unless _has_attribute?(attr_name.to_s)
|
|
162
|
+
|
|
163
|
+
# A hand-written method of the same name may call `super`, which needs
|
|
164
|
+
# the generated method to exist.
|
|
165
|
+
return super if method_defined?(name) || private_method_defined?(name)
|
|
166
|
+
|
|
167
|
+
# Still raise DangerousAttributeError where ActiveRecord would.
|
|
168
|
+
instance_method_already_implemented?(name) unless override
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# What the slow path needs to stand in for the skipped methods.
|
|
173
|
+
module Record
|
|
174
|
+
# ActiveModel's method_missing only knows real attributes: with
|
|
175
|
+
# `alias_attribute :title, :name`, title_changed? must become
|
|
176
|
+
# attribute_changed?("name"). Rails aliases id_value to id on every model.
|
|
177
|
+
def attribute_missing(match, ...)
|
|
178
|
+
return super unless Slim.enabled?
|
|
179
|
+
|
|
180
|
+
# The name comes from a regex match, a new String each time, and the
|
|
181
|
+
# record's attribute set keeps whatever String it gets as a key. Pass
|
|
182
|
+
# the class's own name String, as generated methods do.
|
|
183
|
+
target = self.class.attribute_aliases[match.attr_name] || match.attr_name
|
|
184
|
+
__send__(match.proxy_target, Slim.canonical_name(self.class, target), ...)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
# ActiveModel overrides respond_to? but not respond_to_missing?, so
|
|
190
|
+
# without this `user.method(:name_changed?)` raises NameError.
|
|
191
|
+
def respond_to_missing?(name, include_private = false)
|
|
192
|
+
# Strict: matched_attribute_method only matches readers, writers and
|
|
193
|
+
# the PROBED patterns.
|
|
194
|
+
super || (Slim.enabled? && !matched_attribute_method(name.to_s).nil?)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# ActiveModel's method_missing gives up on anything respond_to? (and so
|
|
198
|
+
# respond_to_missing? above) says exists; dispatch pattern matches here.
|
|
199
|
+
def method_missing(name, ...)
|
|
200
|
+
return super unless Slim.enabled?
|
|
201
|
+
|
|
202
|
+
self.class.define_attribute_methods
|
|
203
|
+
match = matched_attribute_method(name.name)
|
|
204
|
+
unless match
|
|
205
|
+
if Slim.strict? && (replacement = Slim.replacement(self.class, name.name))
|
|
206
|
+
raise NoMethodError.new("#{name} was removed by activerecord-minimizer's strict mode; use #{replacement}",
|
|
207
|
+
name, receiver: self)
|
|
208
|
+
end
|
|
209
|
+
return super
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
Slim.generate(self.class, name.name, match) if Slim.lazy? || Slim::PROBED.value?(match.proxy_target)
|
|
213
|
+
attribute_missing(match, ...)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Strict: only readers and writers are left to match (columns only some
|
|
217
|
+
# query selects, `AS trips_count`), so check those two directly instead
|
|
218
|
+
# of every pattern ActiveModel knows.
|
|
219
|
+
def matched_attribute_method(method_name)
|
|
220
|
+
return super unless Slim.strict?
|
|
221
|
+
|
|
222
|
+
Slim::PROBED.each do |suffix, proxy|
|
|
223
|
+
next unless method_name.end_with?(suffix)
|
|
224
|
+
|
|
225
|
+
attr = method_name.delete_suffix(suffix)
|
|
226
|
+
klass = self.class
|
|
227
|
+
return Slim::MATCH.new(proxy, attr) if klass._has_attribute?(attr) || klass.attribute_aliases.key?(attr) || @attributes&.key?(attr)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
writer = method_name.end_with?("=")
|
|
231
|
+
attr = writer ? method_name.delete_suffix("=") : method_name
|
|
232
|
+
# The class's own attributes have generated readers and writers; what
|
|
233
|
+
# reaches here is columns only this query has, or aliases.
|
|
234
|
+
return unless @attributes&.key?(attr) || self.class.attribute_aliases.key?(attr)
|
|
235
|
+
|
|
236
|
+
Slim::MATCH.new(writer ? "attribute=" : "attribute", attr)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# ActiveModel's matched_attribute_method asks this for each candidate
|
|
240
|
+
# attribute, and only knows the attributes this instance loaded. That
|
|
241
|
+
# still has to work for columns only this query has (`select("COUNT(*)
|
|
242
|
+
# AS trips_count")`), but on a partial select (`select(:id)`) name_changed?
|
|
243
|
+
# would raise NoMethodError where the generated method answers. Accept the
|
|
244
|
+
# class's attributes and aliases too, as the generated methods would.
|
|
245
|
+
def attribute_method?(attr_name)
|
|
246
|
+
return super unless Slim.enabled?
|
|
247
|
+
|
|
248
|
+
# Rails' check materializes the attribute in a lazy attribute set,
|
|
249
|
+
# keyed by the String it gets: give it the class's own.
|
|
250
|
+
attr_name = Slim.canonical_name(self.class, attr_name)
|
|
251
|
+
return true if super
|
|
252
|
+
|
|
253
|
+
klass = self.class
|
|
254
|
+
klass._has_attribute?(attr_name) || klass._has_attribute?(klass.attribute_aliases[attr_name].to_s)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArMinimizer
|
|
4
|
+
# Records that never need a Hash don't get one.
|
|
5
|
+
#
|
|
6
|
+
# A record loaded from the database owns two Hashes that most records never
|
|
7
|
+
# put anything in, 160 bytes each on Ruby 4.0 (a fresh record owns about
|
|
8
|
+
# 1,500 bytes all told):
|
|
9
|
+
#
|
|
10
|
+
# @association_cache filled when an association is loaded
|
|
11
|
+
# @attributes.@attributes filled when an Attribute object is built:
|
|
12
|
+
# dirty tracking, writes, keys, ... Plain reads go
|
|
13
|
+
# through LazyAttributeSet#fetch_value and don't.
|
|
14
|
+
#
|
|
15
|
+
# Both start as one shared frozen empty Hash and get their own the first
|
|
16
|
+
# time something goes in. Only that shared Hash is swapped: attributes
|
|
17
|
+
# frozen by `destroy`/`freeze` still raise FrozenError on write, as in Rails.
|
|
18
|
+
module SmallRecords
|
|
19
|
+
EMPTY = {}.freeze
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def install!
|
|
23
|
+
@enabled = true
|
|
24
|
+
ActiveModel::LazyAttributeSet.prepend(AttributeSet) unless ActiveModel::LazyAttributeSet < AttributeSet
|
|
25
|
+
ActiveRecord::Associations.prepend(Associations) unless ActiveRecord::Associations < Associations
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def disable! = @enabled = false
|
|
29
|
+
|
|
30
|
+
def enabled? = @enabled == true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module AttributeSet
|
|
34
|
+
def initialize(values, types, additional_types, default_attributes, attributes = nil)
|
|
35
|
+
super(values, types, additional_types, default_attributes, attributes || (SmallRecords.enabled? ? EMPTY : {}))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def []=(name, value)
|
|
39
|
+
@attributes = {} if @attributes.equal?(EMPTY)
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write_from_database(name, value)
|
|
44
|
+
@attributes = {} if @attributes.equal?(EMPTY)
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def write_from_user(name, value)
|
|
49
|
+
@attributes = {} if @attributes.equal?(EMPTY) && !frozen?
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def write_cast_value(name, value)
|
|
54
|
+
@attributes = {} if @attributes.equal?(EMPTY)
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reverse_merge!(target_attributes)
|
|
59
|
+
@attributes = {} if @attributes.equal?(EMPTY)
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Rails clones the Hash; a clone of the shared one would be frozen but
|
|
64
|
+
# not the shared one, so writes would raise.
|
|
65
|
+
def initialize_clone(other)
|
|
66
|
+
super
|
|
67
|
+
@attributes = EMPTY if other.instance_variable_get(:@attributes).equal?(EMPTY)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def default_attribute(name, ...)
|
|
73
|
+
@attributes = {} if @attributes.equal?(EMPTY)
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
module Associations
|
|
79
|
+
def initialize_dup(*)
|
|
80
|
+
super
|
|
81
|
+
@association_cache = EMPTY if SmallRecords.enabled?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def init_internals
|
|
87
|
+
super
|
|
88
|
+
@association_cache = EMPTY if SmallRecords.enabled? # Rails' {} is dropped right away
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def association_instance_set(name, association)
|
|
92
|
+
@association_cache = {} if @association_cache.equal?(EMPTY)
|
|
93
|
+
super
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArMinimizer
|
|
4
|
+
# Compiles attribute pattern calls in templates (ERB, or any other handler)
|
|
5
|
+
# the way Build does for app/ and lib/, when ActionView compiles each
|
|
6
|
+
# template, so views need no build step. The index covers the models loaded
|
|
7
|
+
# when a template compiles and is rebuilt when more get loaded; templates
|
|
8
|
+
# compiled before a model loaded leave its calls on the slow path.
|
|
9
|
+
module Views
|
|
10
|
+
class << self
|
|
11
|
+
def install!(index: nil, guards: false)
|
|
12
|
+
@guards = guards
|
|
13
|
+
@fixed_index = index
|
|
14
|
+
@index = nil
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
ActionView::Template.prepend(Template) unless ActionView::Template < Template
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# index: an Index, something that returns one when called (loaded on
|
|
20
|
+
# the first template compile), or nil to index the loaded models.
|
|
21
|
+
def index
|
|
22
|
+
if @fixed_index.respond_to?(:call)
|
|
23
|
+
@mutex.synchronize { @fixed_index = @fixed_index.call if @fixed_index.respond_to?(:call) }
|
|
24
|
+
end
|
|
25
|
+
return @fixed_index if @fixed_index
|
|
26
|
+
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
models = ActiveRecord::Base.descendants.size
|
|
29
|
+
@index = nil unless @models == models
|
|
30
|
+
@models = models
|
|
31
|
+
@index ||= Index.from_models
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def rewrite(source, path)
|
|
36
|
+
Rewriter.new(index: index, path: path, marker: false, guards: @guards).rewrite(source).source
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module Template
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# The whole `def _app_views_...(local_assigns, output_buffer) ... end`,
|
|
44
|
+
# so template locals parse as locals.
|
|
45
|
+
def compiled_source
|
|
46
|
+
Views.rewrite(super, short_identifier)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/ar_minimizer.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "ar_minimizer/version"
|
|
5
|
+
require "ar_minimizer/slim"
|
|
6
|
+
|
|
7
|
+
# ActiveRecord defines, for every column, a reader, a writer and one method per
|
|
8
|
+
# attribute method pattern: name?, name_changed?, name_was, saved_change_to_name?,
|
|
9
|
+
# name_before_type_cast, restore_name!, ... (22 patterns in Rails 8.1).
|
|
10
|
+
#
|
|
11
|
+
# ArMinimizer::Slim stops generating everything except the readers and writers.
|
|
12
|
+
# The missing methods still work: ActiveModel's method_missing matches them
|
|
13
|
+
# against the same patterns and calls the same proxy (attribute_changed?("name")).
|
|
14
|
+
#
|
|
15
|
+
# ArMinimizer::Build rewrites static call sites at deploy time so they don't pay
|
|
16
|
+
# for method_missing:
|
|
17
|
+
#
|
|
18
|
+
# user.name_changed?
|
|
19
|
+
# # becomes
|
|
20
|
+
# (::User.equal?(user.class) ? user.attribute_changed?(-"name") : user.name_changed?)
|
|
21
|
+
module ArMinimizer
|
|
22
|
+
autoload :Index, "ar_minimizer/index"
|
|
23
|
+
autoload :Rewriter, "ar_minimizer/rewriter"
|
|
24
|
+
autoload :Build, "ar_minimizer/build"
|
|
25
|
+
autoload :Views, "ar_minimizer/views"
|
|
26
|
+
autoload :Profile, "ar_minimizer/profile"
|
|
27
|
+
autoload :Manifest, "ar_minimizer/manifest"
|
|
28
|
+
autoload :Enums, "ar_minimizer/enums"
|
|
29
|
+
autoload :Census, "ar_minimizer/census"
|
|
30
|
+
autoload :SmallRecords, "ar_minimizer/small_records"
|
|
31
|
+
autoload :Loader, "ar_minimizer/loader"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
require "ar_minimizer/railtie" if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activerecord-minimizer
|
|
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
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '8.3'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '8.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '8.3'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: prism
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.2'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '1.2'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: require-hooks
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0.4'
|
|
53
|
+
type: :runtime
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0.4'
|
|
60
|
+
description: Generates only the reader and writer per Active Record column (strict
|
|
61
|
+
mode removes the other 19 attribute methods), compiles call sites to the calls those
|
|
62
|
+
methods stood for, turns enum closures into plain methods and stops records allocating
|
|
63
|
+
Hashes they never fill.
|
|
64
|
+
executables: []
|
|
65
|
+
extensions: []
|
|
66
|
+
extra_rdoc_files: []
|
|
67
|
+
files:
|
|
68
|
+
- CHANGELOG.md
|
|
69
|
+
- LICENSE.txt
|
|
70
|
+
- README.md
|
|
71
|
+
- lib/activerecord-minimizer.rb
|
|
72
|
+
- lib/ar_minimizer.rb
|
|
73
|
+
- lib/ar_minimizer/build.rb
|
|
74
|
+
- lib/ar_minimizer/census.rb
|
|
75
|
+
- lib/ar_minimizer/enums.rb
|
|
76
|
+
- lib/ar_minimizer/index.rb
|
|
77
|
+
- lib/ar_minimizer/loader.rb
|
|
78
|
+
- lib/ar_minimizer/manifest.rb
|
|
79
|
+
- lib/ar_minimizer/profile.rb
|
|
80
|
+
- lib/ar_minimizer/railtie.rb
|
|
81
|
+
- lib/ar_minimizer/rewriter.rb
|
|
82
|
+
- lib/ar_minimizer/slim.rb
|
|
83
|
+
- lib/ar_minimizer/small_records.rb
|
|
84
|
+
- lib/ar_minimizer/version.rb
|
|
85
|
+
- lib/ar_minimizer/views.rb
|
|
86
|
+
homepage: https://github.com/sampokuokkanen/rails-minify/tree/main/activerecord-minimizer
|
|
87
|
+
licenses:
|
|
88
|
+
- MIT
|
|
89
|
+
metadata:
|
|
90
|
+
source_code_uri: https://github.com/sampokuokkanen/rails-minify/tree/main/activerecord-minimizer
|
|
91
|
+
changelog_uri: https://github.com/sampokuokkanen/rails-minify/blob/main/activerecord-minimizer/CHANGELOG.md
|
|
92
|
+
bug_tracker_uri: https://github.com/sampokuokkanen/rails-minify/issues
|
|
93
|
+
rubygems_mfa_required: 'true'
|
|
94
|
+
rdoc_options: []
|
|
95
|
+
require_paths:
|
|
96
|
+
- lib
|
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '3.4'
|
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0'
|
|
107
|
+
requirements: []
|
|
108
|
+
rubygems_version: 4.0.20
|
|
109
|
+
specification_version: 4
|
|
110
|
+
summary: Stop generating ~20 attribute methods per column; compile the call sites
|
|
111
|
+
instead
|
|
112
|
+
test_files: []
|