From baddab8156e29a2615d1a3944e1272a90a525478 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Sun, 5 Jan 2025 10:32:25 -0500 Subject: [PATCH] Document how to support camelcase attributes Generally, this commit adds documentation to the `ActiveResource::Base` class-level and method-level documentation to explain how attribute loading and encoding works. In addition, test coverage is added for camelcase-based attribute loading and encoding. Code samples are added to method-level documentation to more clearly communicate how support can be added at the application level. --- lib/active_resource/base.rb | 77 +++++++++++++++++++++++++++++++++++- test/cases/base/load_test.rb | 12 ++++++ test/cases/base_test.rb | 10 +++++ test/fixtures/person.rb | 14 +++++++ 4 files changed, 112 insertions(+), 1 deletion(-) diff --git a/lib/active_resource/base.rb b/lib/active_resource/base.rb index 29376f081d..85fa88af96 100644 --- a/lib/active_resource/base.rb +++ b/lib/active_resource/base.rb @@ -54,6 +54,48 @@ module ActiveResource # self.proxy = "https://user:password@proxy.people.com:8080" # end # + # == Attribute Schema + # + # Active Resource objects respond to attributes returned by +known_attributes+. + # Resource instances populate the +known_attributes+ array during calls to +initialize+ and +load, + # or when data is fetched from the remote system. + # + # Instances will not respond to attribute methods until they are loaded. + # + # Resource classes can define the set of +known_attributes+ by defining a +schema+: + # + # class Person < ActiveResource::Base + # schema do + # # define each attribute separately + # attribute :name, :string + # attribute :age, :integer + # end + # end + # + # p = Person.new + # p.respond_to? :name # => true + # p.respond_to? :name= # => true + # p.respond_to? :age # => true + # p.respond_to? :age= # => true + # p.name # => nil + # p.age # => nil + # + # Attribute-types must be one of: string, text, integer, float, decimal, datetime, timestamp, time, date, binary, boolean + # + # To transform data fetched from the remote system prior to attribute + # assignment, override the +load+ method: + # + # class Person < ActiveResource::Base + # def load(attributes, remove_root = false, persisted = false) + # attributes = attributes.deep_transform_keys { |key| key.to_s.underscore } + # + # super + # end + # end + # + # p = Person.new(:firstName => 'Ryan', :lastName => 'Daigle') + # p.first_name # => 'Ryan' + # p.last_name # => 'Daigle' # # == Life cycle methods # @@ -1425,7 +1467,23 @@ def exists? # Returns the serialized string representation of the resource in the configured # serialization format specified in ActiveResource::Base.format. The options - # applicable depend on the configured encoding format. + # applicable depend on the configured encoding format, and are forwarded to + # the corresponding serializer method. + # + # ActiveResource::Formats::XmlFormat will forward options #to_xml and + # ActiveResource::Formats::JsonFormat will forward options #to_json. + # + # Override the +serializable_hash+ method to customize how the + # attribute keys and values are encoded to a JSON or XML string. + # + # class Person < ActiveResource::Base + # def serializable_hash(options = {}) + # super.deep_transform_keys! { |key| key.camelcase(:lower) } + # end + # end + # + # A resource that relies on a custom format must respond to a serializer method + # that corresponds to the format's +extension+ method. def encode(options = {}) send("to_#{self.class.format.extension}", options) end @@ -1466,6 +1524,23 @@ def reload # your_supplier = Supplier.new # your_supplier.load(my_attrs) # your_supplier.save + # + # Override +load+ to transform the attributes prior to loading. + # + # class Person < ActiveResource::Base + # def load(attributes, remove_root = false, persisted = false) + # attributes = attributes.deep_transform_keys { |key| key.to_s.underscore } + # + # super + # end + # end + # + # camelcase_attrs = { :firstName => 'Marty', :favorite_colors => ['red', 'green', 'blue'] } + # + # person = Person.new + # person.load(camelcase_attrs) + # person.first_name # => 'Marty' + # person.favorite_colors # => ['red', 'green', 'blue'] def load(attributes, remove_root = false, persisted = false) unless attributes.respond_to?(:to_hash) raise ArgumentError, "expected attributes to be able to convert to Hash, got #{attributes.inspect}" diff --git a/test/cases/base/load_test.rb b/test/cases/base/load_test.rb index 94e2915228..0ba7b4c8ed 100644 --- a/test/cases/base/load_test.rb +++ b/test/cases/base/load_test.rb @@ -123,6 +123,18 @@ def test_load_simple_hash assert_equal @matz.stringify_keys, @person.load(@matz).attributes end + def test_load_simple_camelcase_hash + camelcase_person = Camelcase::Person.new(likesHats: true) + + assert_predicate camelcase_person, :likes_hats? + end + + def test_load_nested_camelcase_hash + camelcase_person = Camelcase::Person.new(address: { streetName: "12345 Street" }) + + assert_equal "12345 Street", camelcase_person.address.street_name + end + def test_load_object_with_implicit_conversion_to_hash assert_equal @matz.stringify_keys, @person.load(FakeParameters.new(@matz)).attributes end diff --git a/test/cases/base_test.rb b/test/cases/base_test.rb index 3689b4234d..5312e4c4de 100644 --- a/test/cases/base_test.rb +++ b/test/cases/base_test.rb @@ -1471,6 +1471,16 @@ def test_to_json_with_element_name Person.element_name = old_elem_name end + def test_to_json_with_camelcase + joe_camel = Camelcase::Person.new(name: "Joe", likes_hats: true) + encode = joe_camel.encode + json = joe_camel.to_json + + assert_equal encode, json + assert_match %r{"name":"Joe"}, json + assert_match %r{"likesHats":true}, json + end + def test_to_param_quacks_like_active_record new_person = Person.new assert_nil new_person.to_param diff --git a/test/fixtures/person.rb b/test/fixtures/person.rb index ad0c20ada8..742c330cda 100644 --- a/test/fixtures/person.rb +++ b/test/fixtures/person.rb @@ -13,3 +13,17 @@ class ProfileData < ActiveResource::Base self.site = "http://external.profile.data.nl" end end + +module Camelcase + class Person < ::Person + def load(attributes, *args) + attributes = attributes.deep_transform_keys { |key| key.to_s.underscore } + + super + end + + def serializable_hash(options = {}) + super.deep_transform_keys! { |key| key.camelcase(:lower) } + end + end +end