From 72a884d7fa4972e07c6c34dc924a476665780b0f Mon Sep 17 00:00:00 2001 From: OKURA Masafumi Date: Sun, 23 Jul 2023 02:55:09 +0900 Subject: [PATCH 1/2] [Lint] Allow `alias_method` not having parentheses --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 129bb70..cb322f2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -119,7 +119,7 @@ Style/InlineComment: Enabled: false Style/MethodCallWithArgsParentheses: - AllowedMethods: ['require', 'require_relative', 'include', 'extend', 'puts', 'p', 'warn', 'raise', 'send', 'public_send'] + AllowedMethods: ['require', 'require_relative', 'include', 'extend', 'puts', 'p', 'warn', 'raise', 'send', 'public_send', 'alias_method'] Exclude: # There are so many `attributes` call without parenthese and that's absolutely fine - 'test/**/*.rb' From ec58245e43ed20db0dfd88c9d98112f5c97be0d9 Mon Sep 17 00:00:00 2001 From: OKURA Masafumi Date: Sun, 23 Jul 2023 02:55:59 +0900 Subject: [PATCH 2/2] [Feat] Add `prefer_resource_method!` This is a way to change current behavior of when there are two methods with the same name, one on the resource and one on the object. Some say they prefer defining methods instead of using `attribute` since it's easier to type methods than type DSL. The problem is that methods on resource are not called if there's a method with the same name on the object. I understand this situation and I'd like to support it. --- README.md | 66 +++++++++++++++++ lib/alba/resource.rb | 21 ++++++ .../object_method_and_resource_method_test.rb | 73 +++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 test/usecases/object_method_and_resource_method_test.rb diff --git a/README.md b/README.md index fbfd277..f078741 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,72 @@ class UserResource end ``` +#### Prefer methods on resource + +By default, Alba prefers methods on the object to methods on the resource. This means if you have a following situation: + +```ruby +class User + attr_accessor :id, :name, :email + + def initialize(id, name, email) + @id = id + @name = name + @email = email + end + + def name_with_email + "dummy!" + end +end + +class UserResource + include Alba::Resource + + root_key :user, :users # Later is for plural + + attributes :id, :name, :name_with_email + + # Same method exists in `User` class! + # This is not called + def name_with_email(user) + "#{user.name}: #{user.email}" + end +end + +user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com') +UserResource.new(user).serialize +# => '{"user":{"id":1,"name":"Masafumi OKURA","name_with_email":"dummy!"}}' +``` + +You can see that `name_with_email` is now `dummy!` from `User#name_with_email`. You cna change this behavior by using `prefer_resource_method!` DSL in a resource class: + +```ruby +# With the same `User` class + +class UserResource + include Alba::Resource + + prefer_resource_method! # This line is important + + root_key :user, :users # Later is for plural + + attributes :id, :name, :name_with_email + + # Same method exists in `User` class! + # But now this is called! + def name_with_email(user) + "#{user.name}: #{user.email}" + end +end + +user = User.new(1, 'Masafumi OKURA', 'masafumi@example.com') +UserResource.new(user).serialize +# => '{"user":{"id":1,"name":"Masafumi OKURA","name_with_email":"Masafumi OKURA: masafumi@example.com"}}' +``` + +The next major version of Alba will change this default behavior to prefer resource methods. In case you want to preserve current behavior, there's `prefer_object_method!` DSL, which does that. + #### Params You can pass a Hash to the resource for internal use. It can be used as "flags" to control attribute content. diff --git a/lib/alba/resource.rb b/lib/alba/resource.rb index f65de17..0b83a34 100644 --- a/lib/alba/resource.rb +++ b/lib/alba/resource.rb @@ -265,12 +265,23 @@ def fetch_attribute(obj, key, attribute) # rubocop:disable Metrics/CyclomaticCom value.nil? && nil_handler ? instance_exec(obj, key, attribute, &nil_handler) : value end + # TODO: from version 3, `_fetch_attribute_from_resource_first` is default def fetch_attribute_from_object_and_resource(obj, attribute) + _fetch_attribute_from_object_first(obj, attribute) + end + + def _fetch_attribute_from_object_first(obj, attribute) obj.__send__(attribute) rescue NoMethodError __send__(attribute, obj) end + def _fetch_attribute_from_resource_first(obj, attribute) + __send__(attribute, obj) + rescue NoMethodError + obj.__send__(attribute) + end + def nil_handler @_on_nil end @@ -510,6 +521,16 @@ def helper(mod = @_helper || Module.new, &block) extend mod @_helper = mod end + + # DSL for alias, purely for readability + def prefer_resource_method! + alias_method :fetch_attribute_from_object_and_resource, :_fetch_attribute_from_resource_first + end + + # DSL for alias, purely for readability + def prefer_object_method! + alias_method :fetch_attribute_from_object_and_resource, :_fetch_attribute_from_object_first + end end end end diff --git a/test/usecases/object_method_and_resource_method_test.rb b/test/usecases/object_method_and_resource_method_test.rb new file mode 100644 index 0000000..2ca649b --- /dev/null +++ b/test/usecases/object_method_and_resource_method_test.rb @@ -0,0 +1,73 @@ +require_relative '../test_helper' + +class ObjectMethodAndResourceMethodTest < Minitest::Test + class Foo + attr_reader :id + def initialize(id) + @id = id + end + end + + def setup + @foo = Foo.new(1) + end + + class FooResource + include Alba::Resource + + prefer_resource_method! + + attributes :id + + def id(_) + 42 + end + end + + def test_prefer_resource_method + assert_equal '{"id":42}', FooResource.new(@foo).serialize + end + + class FooResource2 + include Alba::Resource + + prefer_object_method! + + attributes :id + + def id(_) + 42 + end + end + + def test_prefer_object_method + assert_equal '{"id":1}', FooResource2.new(@foo).serialize + end + + class FooResource3 + include Alba::Resource + + attributes :id + + def id(_) + 42 + end + end + + # TODO: perfer resource method by default from version 3 + def test_default_behavior + assert_equal '{"id":1}', FooResource3.new(@foo).serialize + end + + class FooResource4 + include Alba::Resource + + prefer_resource_method! + + attributes :id + end + + def test_prefer_resource_method_but_it_is_not_there + assert_equal '{"id":1}', FooResource4.new(@foo).serialize + end +end