From d50c3f693793f074baca23a9e97257bcaecd74a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Rodr=C3=ADguez?= Date: Mon, 16 Feb 2026 15:16:35 +0100 Subject: [PATCH] Performance improvements in class_methods.rb Makes a series of performance improvements to the methods defined by `#attr_accessor_type`. In most cases this will make the initial loading when defining a class a bit slower but make parsing and dumping faster. - Use a shared ValueHelper singleton that is shared by all closures - Remove calls to #get_attribute: use the cka closure directly - Pre-compute getter and setter symbols (:"@name" and :"@name=") - Define smaller methods, changing the logic depending on the type and other things known at definition time. This makes the runtime checks leaner. For example, the old code generated methods that had to check on every run whether the attribute was `one_of`. This is solved by checking before defining the method. --- class_kit.gemspec | 6 +- lib/class_kit/class_methods.rb | 141 +++++++++++++++++++++------------ 2 files changed, 93 insertions(+), 54 deletions(-) diff --git a/class_kit.gemspec b/class_kit.gemspec index 14c0f0a..8643ea3 100644 --- a/class_kit.gemspec +++ b/class_kit.gemspec @@ -1,4 +1,4 @@ -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'class_kit/version' @@ -13,14 +13,14 @@ Gem::Specification.new do |spec| spec.homepage = 'https://github.com/sage/class_kit' spec.license = 'MIT' - spec.files = Dir.glob("{bin,lib}/**/**/**") + spec.files = Dir.glob('{bin,lib}/**/**/**') spec.bindir = 'exe' spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - spec.add_development_dependency 'bundler', '~> 2' spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rspec' + spec.add_development_dependency 'rubocop' spec.add_development_dependency 'simplecov', ' ~> 0.22.0' spec.add_development_dependency 'simplecov_json_formatter' diff --git a/lib/class_kit/class_methods.rb b/lib/class_kit/class_methods.rb index e96506f..c93c0cb 100644 --- a/lib/class_kit/class_methods.rb +++ b/lib/class_kit/class_methods.rb @@ -8,15 +8,18 @@ def attr_accessor_type( default: nil, auto_init: false, alias_name: nil, - meta: {}) - unless instance_variable_defined?(:@class_kit_attributes) - instance_variable_set(:@class_kit_attributes, {}) - end + meta: {} + ) + instance_variable_set(:@class_kit_attributes, {}) unless instance_variable_defined?(:@class_kit_attributes) attributes = instance_variable_get(:@class_kit_attributes) - attributes[name] = { + getter = :"@#{name}" + setter = :"#{name}=" + + cka = { name: name, + setter: setter, type: type, one_of: one_of, collection_type: collection_type, @@ -25,68 +28,104 @@ def attr_accessor_type( auto_init: auto_init, alias: alias_name, meta: meta - } - - class_eval do - define_method name do + }.freeze + attributes[name] = cka - cka = ClassKit::AttributeHelper.instance.get_attribute(klass: self.class, name: name) + value_helper = ClassKit::ValueHelper.instance - current_value = instance_variable_get(:"@#{name}") + # ========= Define attribute getter ========= + if !default.nil? || auto_init + class_eval do + define_method name do + current_value = instance_variable_get(getter) - if current_value.nil? - if !cka[:default].nil? - current_value = instance_variable_set(:"@#{name}", cka[:default]) - elsif cka[:auto_init] - current_value = instance_variable_set(:"@#{name}", cka[:type].new) + if current_value.nil? + if !cka[:default].nil? + current_value = instance_variable_set(getter, cka[:default]) + elsif cka[:auto_init] + current_value = instance_variable_set(getter, cka[:type].new) + end end - end - current_value + current_value + end end - end - - class_eval do - define_method "#{name}=" do |value| - # get the attribute meta data - cka = ClassKit::AttributeHelper.instance.get_attribute(klass: self.class, name: name) - - # verify if the attribute is allowed to be set to nil - if value.nil? && cka[:allow_nil] == false - raise ClassKit::Exceptions::InvalidAttributeValueError, "Attribute: #{name}, must not be nil." + else + # no default or auto_init: just return the variable + class_eval do + define_method(name) do + instance_variable_get(getter) end + end + end - if !cka[:one_of].nil? && !value.nil? - parsed_value = - if value == true || value == false - value - elsif(/(true|t|yes|y|1)$/i === value.to_s.downcase) - true - elsif (/(false|f|no|n|0)$/i === value.to_s.downcase) - false + # ========= Define attribute setter ========= + # The methods are defined as lean as possible + # This is a bit harder to read at this level but it gets rid of unnecessary runtime checks + # 1. If one_of is specified, we check if the value matches any of the allowed types + if one_of + class_eval do + define_method "#{name}=" do |value| + if value.nil? && cka[:allow_nil] == false + raise ClassKit::Exceptions::InvalidAttributeValueError, "Attribute: #{name}, must not be nil." end - if parsed_value != nil - value = parsed_value - else + value = if value.nil? + value + elsif [true, false].include?(value) + value + elsif Constants::BOOL_TRUE_RE.match?(value.to_s) + true + elsif Constants::BOOL_FALSE_RE.match?(value.to_s) + false + else + begin + t = cka[:one_of].detect { |t| value.is_a?(t) } + value = value_helper.parse(type: t, value: value) + rescue StandardError => e + raise ClassKit::Exceptions::InvalidAttributeValueError, + "Attribute: #{name}, must be of type: #{t}. Error: #{e}" + end + end + + instance_variable_set(getter, value) + end + end + # 2. When the attribute is typed, we parse into the target type if needed + elsif type + class_eval do + define_method "#{name}=" do |value| + if value.nil? + if cka[:allow_nil] == false + raise ClassKit::Exceptions::InvalidAttributeValueError, "Attribute: #{name}, must not be nil." + end + elsif type == :bool || !value.is_a?(type) begin - type = cka[:one_of].detect {|t| value.is_a?(t) } - value = ClassKit::ValueHelper.instance.parse(type: type, value: value) - rescue => e - raise ClassKit::Exceptions::InvalidAttributeValueError, "Attribute: #{name}, must be of type: #{type}. Error: #{e}" + value = value_helper.parse(type: type, value: value) + rescue StandardError => e + raise ClassKit::Exceptions::InvalidAttributeValueError, + "Attribute: #{name}, must be of type: #{type}. Error: #{e}" end end - end - if !cka[:type].nil? && !value.nil? && (cka[:type] == :bool || !value.is_a?(cka[:type])) - begin - value = ClassKit::ValueHelper.instance.parse(type: cka[:type], value: value) - rescue => e - raise ClassKit::Exceptions::InvalidAttributeValueError, "Attribute: #{name}, must be of type: #{cka[:type]}. Error: #{e}" - end + instance_variable_set(getter, value) + end + end + # 3. If untyped and we allow nil, simply set the variable + elsif allow_nil == true + class_eval do + define_method "#{name}=" do |value| + instance_variable_set(getter, value) end + end + # 4. In all other cases, only set the variable if non-nil + else + class_eval do + define_method "#{name}=" do |value| + raise ClassKit::Exceptions::InvalidAttributeValueError, "Attribute: #{name}, must not be nil." if value.nil? - instance_variable_set(:"@#{name}", value) + instance_variable_set(getter, value) + end end end end