diff --git a/Gemfile.lock b/Gemfile.lock index e0a0a29..c155081 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - activemodel-entity (0.8.0) + activemodel-entity (0.13.0) actionpack (>= 7) activemodel (>= 7) activesupport (>= 7) @@ -69,19 +69,23 @@ GEM loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - minitest (5.24.1) + mini_portile2 (2.8.9) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) mutex_m (0.2.0) - nokogiri (1.16.6-aarch64-linux) + nokogiri (1.19.4) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.6-arm-linux) + nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.16.6-arm64-darwin) + nokogiri (1.19.4-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.16.6-x86-linux) + nokogiri (1.19.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.6-x86_64-darwin) + nokogiri (1.19.4-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.6-x86_64-linux) + nokogiri (1.19.4-x86_64-linux-gnu) racc (~> 1.4) parallel (1.25.1) parser (3.3.4.0) @@ -90,6 +94,7 @@ GEM pp (0.6.2) prettyprint prettyprint (0.2.0) + prism (1.9.0) psych (5.2.6) date stringio diff --git a/lib/active_model/entity/parsers/json.rb b/lib/active_model/entity/parsers/json.rb index d4b9596..b1e7902 100644 --- a/lib/active_model/entity/parsers/json.rb +++ b/lib/active_model/entity/parsers/json.rb @@ -11,6 +11,10 @@ def set_attribute_from_json(name, value) @attributes[name] = @attributes[name].with_value_from_json(value) end + def assign_attributes_from_json(json) + instance_exec(json, &self.class.json_attributes_assigner) + end + # Class-level methods. module ClassMethods def from_json(json) @@ -18,16 +22,23 @@ def from_json(json) instance.assign_attributes_from_json(json) end end - end - def method_missing(method_name, *, &) - if method_name == :assign_attributes_from_json - setters = attributes.keys.map do |name| + # Compiled once per exact class (class-level ivars are not + # inherited), so an assigner compiled for a superclass can never + # shadow attributes that exist only on a subclass. + def json_attributes_assigner + @json_attributes_assigner ||= compile_json_attributes_assigner + end + + private + + def compile_json_attributes_assigner + setters = attribute_types.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( + set_attribute_from_json( #{name.inspect}, #{name}_value ) @@ -35,21 +46,14 @@ def method_missing(method_name, *, &) end code = <<~RUBY - def assign_attributes_from_json(json) + ->(json) do #{setters.join("\n")} end RUBY - self.class.class_eval(code) - send(method_name, *, &) - else - super + class_eval(code, __FILE__, __LINE__) end end - - def respond_to_missing?(method_name, include_private) - method_name == :assign_attributes_from_json || super - end end end end diff --git a/spec/active_model/entity/parsers/json_spec.rb b/spec/active_model/entity/parsers/json_spec.rb index 0fd43b0..1d6d696 100644 --- a/spec/active_model/entity/parsers/json_spec.rb +++ b/spec/active_model/entity/parsers/json_spec.rb @@ -17,6 +17,16 @@ class Role attribute :field_positions, :array, of: "ParsersTest::Position" end + class Employee + include ActiveModel::Entity + + attribute :field_name, :string + end + + class Manager < Employee + attribute :field_department, :string + end + class Person include ActiveModel::Entity @@ -121,6 +131,17 @@ class Person end end + context "parsing into a subclass after the superclass has compiled its assigner" do + it "keeps subclass-only attributes" do + employee = ParsersTest::Employee.from_json({ "fieldName" => "emp" }) + manager = ParsersTest::Manager.from_json({ "fieldName" => "mgr", "fieldDepartment" => "sales" }) + + expect(employee.field_name).to eq("emp") + expect(manager.field_name).to eq("mgr") + expect(manager.field_department).to eq("sales") + end + end + context "parsing symbolized json" do let(:source) do {