From f54ba410dc6e79fe42664725bc3a16e0fb42664a Mon Sep 17 00:00:00 2001 From: Taleh Zaliyev Date: Wed, 23 Jul 2025 17:37:45 +0300 Subject: [PATCH 1/3] [BAC-381] Enhance from_json with aliases --- lib/active_model/entity.rb | 2 ++ lib/active_model/entity/camel_caseable.rb | 27 +++++++++++++++++++++++ lib/active_model/entity/parsers/json.rb | 9 ++++---- 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 lib/active_model/entity/camel_caseable.rb diff --git a/lib/active_model/entity.rb b/lib/active_model/entity.rb index a9abca8..4cea8f1 100644 --- a/lib/active_model/entity.rb +++ b/lib/active_model/entity.rb @@ -17,6 +17,7 @@ require_relative "entity/validations/exclusive_presence_validator" require_relative "entity/validations/validates_nested" require_relative "entity/pattern_matcheable" +require_relative "entity/camel_caseable" # Load Railtie if Rails is defined require_relative "entity/railtie" if defined?(Rails::Railtie) @@ -39,6 +40,7 @@ module Entity include ActiveModel::Entity::Validations include ActiveModel::Entity::Validations::ValidatesNested include ActiveModel::Entity::PatternMatcheable + include ActiveModel::Entity::CamelCaseable end end end diff --git a/lib/active_model/entity/camel_caseable.rb b/lib/active_model/entity/camel_caseable.rb new file mode 100644 index 0000000..a34ca58 --- /dev/null +++ b/lib/active_model/entity/camel_caseable.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module ActiveModel + module Entity + # Adds alias for attribute with camelcase name + module CamelCaseable + extend ActiveSupport::Concern + + included do + class_attribute :camelized_attributes_names, default: [] + end + + module ClassMethods + def attribute(name, ...) + super + + camelize_name = name.to_s.camelize(:lower) + camelized_attributes_names.push(camelize_name) + + return unless name.to_s != camelize_name + + alias_attribute camelize_name.to_sym, name + end + end + end + end +end diff --git a/lib/active_model/entity/parsers/json.rb b/lib/active_model/entity/parsers/json.rb index aa7c30f..28b34d9 100644 --- a/lib/active_model/entity/parsers/json.rb +++ b/lib/active_model/entity/parsers/json.rb @@ -8,7 +8,8 @@ module JSON extend ActiveSupport::Concern def set_attribute_from_json(name, value) - @attributes[name] = @attributes[name].with_value_from_json(value) + original_attribute_name = attribute_aliases.fetch(name, name) + @attributes[original_attribute_name] = @attributes[original_attribute_name].with_value_from_json(value) end # Class-level methods. @@ -16,10 +17,10 @@ module ClassMethods def from_json(json) unified_json = json.stringify_keys new.tap do |instance| - instance.attributes.each_key do |name| - field_name = name.camelize(:lower) + instance.camelized_attributes_names.each do |name| + next unless unified_json.key?(name) - instance.set_attribute_from_json(name, unified_json[field_name]) + instance.set_attribute_from_json(name, unified_json[name]) end end end From 2758948958d3724047ae455a7b7231d58092912b Mon Sep 17 00:00:00 2001 From: Taleh Zaliyev Date: Wed, 23 Jul 2025 18:41:19 +0300 Subject: [PATCH 2/3] [BAC-381] Metamagic optimization --- lib/active_model/entity.rb | 2 -- lib/active_model/entity/camel_caseable.rb | 27 ----------------- lib/active_model/entity/parsers/json.rb | 35 ++++++++++++++++++----- 3 files changed, 28 insertions(+), 36 deletions(-) delete mode 100644 lib/active_model/entity/camel_caseable.rb diff --git a/lib/active_model/entity.rb b/lib/active_model/entity.rb index 4cea8f1..a9abca8 100644 --- a/lib/active_model/entity.rb +++ b/lib/active_model/entity.rb @@ -17,7 +17,6 @@ require_relative "entity/validations/exclusive_presence_validator" require_relative "entity/validations/validates_nested" require_relative "entity/pattern_matcheable" -require_relative "entity/camel_caseable" # Load Railtie if Rails is defined require_relative "entity/railtie" if defined?(Rails::Railtie) @@ -40,7 +39,6 @@ module Entity include ActiveModel::Entity::Validations include ActiveModel::Entity::Validations::ValidatesNested include ActiveModel::Entity::PatternMatcheable - include ActiveModel::Entity::CamelCaseable end end end diff --git a/lib/active_model/entity/camel_caseable.rb b/lib/active_model/entity/camel_caseable.rb deleted file mode 100644 index a34ca58..0000000 --- a/lib/active_model/entity/camel_caseable.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module ActiveModel - module Entity - # Adds alias for attribute with camelcase name - module CamelCaseable - extend ActiveSupport::Concern - - included do - class_attribute :camelized_attributes_names, default: [] - end - - module ClassMethods - def attribute(name, ...) - super - - camelize_name = name.to_s.camelize(:lower) - camelized_attributes_names.push(camelize_name) - - return unless name.to_s != camelize_name - - alias_attribute camelize_name.to_sym, name - end - end - end - end -end diff --git a/lib/active_model/entity/parsers/json.rb b/lib/active_model/entity/parsers/json.rb index 28b34d9..82da664 100644 --- a/lib/active_model/entity/parsers/json.rb +++ b/lib/active_model/entity/parsers/json.rb @@ -8,21 +8,42 @@ module JSON extend ActiveSupport::Concern def set_attribute_from_json(name, value) - original_attribute_name = attribute_aliases.fetch(name, name) - @attributes[original_attribute_name] = @attributes[original_attribute_name].with_value_from_json(value) + @attributes[name] = @attributes[name].with_value_from_json(value) end # Class-level methods. module ClassMethods def from_json(json) - unified_json = json.stringify_keys new.tap do |instance| - instance.camelized_attributes_names.each do |name| - next unless unified_json.key?(name) + instance.assign_attributes_from_json(json) + end + end + end - instance.set_attribute_from_json(name, unified_json[name]) - end + def method_missing(method_name, *args, &block) + if method_name == :assign_attributes_from_json + setters = self.attributes.keys.map do |name| + <<~RUBY + #{name}_value = json[#{name.camelize(:lower).inspect}] + #{name}_value = json[#{name.camelize(:lower).to_sym.inspect}] if #{name}_value.nil? + + self.set_attribute_from_json( + #{name.inspect}, + #{name}_value + ) + RUBY end + + code = <<~RUBY + def assign_attributes_from_json(json) + #{setters.join("\n")} + end + RUBY + + self.class.class_eval(code) + self.send(method_name, *args, &block) + else + super end end end From c9dcceca2359db2627ed39da5553302843b76ab9 Mon Sep 17 00:00:00 2001 From: Taleh Zaliyev Date: Wed, 23 Jul 2025 18:45:53 +0300 Subject: [PATCH 3/3] [BAC-381] Fix rubocop --- lib/active_model/entity/parsers/json.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/active_model/entity/parsers/json.rb b/lib/active_model/entity/parsers/json.rb index 82da664..d4b9596 100644 --- a/lib/active_model/entity/parsers/json.rb +++ b/lib/active_model/entity/parsers/json.rb @@ -20,9 +20,9 @@ def from_json(json) end end - def method_missing(method_name, *args, &block) + def method_missing(method_name, *, &) if method_name == :assign_attributes_from_json - setters = self.attributes.keys.map do |name| + setters = attributes.keys.map do |name| <<~RUBY #{name}_value = json[#{name.camelize(:lower).inspect}] #{name}_value = json[#{name.camelize(:lower).to_sym.inspect}] if #{name}_value.nil? @@ -41,11 +41,15 @@ def assign_attributes_from_json(json) RUBY self.class.class_eval(code) - self.send(method_name, *args, &block) + send(method_name, *, &) else super end end + + def respond_to_missing?(method_name, include_private) + method_name == :assign_attributes_from_json || super + end end end end