Repository: okuramasafumi/alba (Ruby)

Issue:
When typing instance defined attributes, you get a method_missing' error if the passed object does not have that method defined.
## Describe the bug

Typing attributes that are defined on a resource by instance methods doesn't seem supported. This came up when trying to add https://github.com/skryukov/typelizer to our current project.

Ideally, we would be able to support adding types to all attributes, in my case, allowing us to generate the correct type definitions on the other side. 

## To Reproduce
Here is an example:

```ruby
class Admin
  def full_name
    "The Admin's name"
  end
end

class AdminResource
  include Alba::Resource

  attributes full_name: [String, true], message: [String, true]

  def message
    "My message"
  end
end

resource = AdminResource.new(Admin.new)
```
Results in this error: `'method_missing': undefined method 'message' for an instance of Admin`

## Expected behavior
I would have expected the resource to default to call the instance method first before going to the object passed into the resource. I would have expected typing the attribute to change the resource's calling behavior. 

The metadata about the resource is correct when expecting the resource object, but we're unable to serialize the data. (aka call `.to_h` to `.to_json` on the resource object.

## Actual behavior
We get a method missing error on the object passed to the resource object.

## Environment
- Ruby version: 3.3 (but it seems like all versions of Ruby would be affected based on the error)
- present in the latest version of alba: 3.3.2

## Additional context
I'm happy to create a change to solve this issue if you are good with supporting this behavior. 😄 


Discussion:
Upon some more testing, it seems like instance defined methods on the resource aren't used when an attribute is type:

Another Example:

```ruby
class Admin
  def full_name
    "The Admin's name"
  end
  
  def message
    "Original message"
  end
end

class AdminResource
  include Alba::Resource

  attributes full_name: [String, true], message: [String, true]

  def message(object)
    object.message + " from resource"
  end
end

resource = AdminResource.new(Admin.new).to_h

puts resource
# => {"full_name"=>"The Admin's name", "message"=>"Original message"}
```

These two things sound related to the same thing. Though, this seems like it could lead to some undiscovered bugs.


@patvice Thank you for the report. So, the mechanism of "preferring methods defined on resource classes" seems working only for simple attributes and not working for type attributes and conditional attributes.
I recognize this as an issue, but I'm not sure yet about what fix is the cleanest. It'll take some time to come up with good ideas.
Thanks for solving this! 😄 
@patvice https://rubygems.org/gems/alba/versions/3.3.3 version 3.3.3 will fix this issue so please check it out!



Patch:
diff --git a/lib/alba/resource.rb b/lib/alba/resource.rb
index 6695a69..5b3f530 100644
--- a/lib/alba/resource.rb
+++ b/lib/alba/resource.rb
@@ -239,12 +239,12 @@ def transform_key(key)
         Alba.transform_key(key, transform_type: @_transform_type)
       end
 
-      def fetch_attribute(obj, key, attribute) # rubocop:disable Metrics/CyclomaticComplexity
+      def fetch_attribute(obj, key, attribute) # rubocop:disable Metrics
         value = case attribute
                 when Symbol then fetch_attribute_from_object_and_resource(obj, attribute)
                 when Proc then instance_exec(obj, &attribute)
                 when Alba::Association then yield_if_within(attribute.name.to_sym) { |within| attribute.to_h(obj, params: params, within: within) }
-                when TypedAttribute then attribute.value(obj)
+                when TypedAttribute then attribute.value { |attr| fetch_attribute(obj, key, attr) }
                 when NestedAttribute then attribute.value(object: obj, params: params, within: @within)
                 when ConditionalAttribute then attribute.with_passing_condition(resource: self, object: obj) { |attr| fetch_attribute(obj, key, attr) }
                   # :nocov:
diff --git a/lib/alba/typed_attribute.rb b/lib/alba/typed_attribute.rb
index 30923d2..09f7146 100644
--- a/lib/alba/typed_attribute.rb
+++ b/lib/alba/typed_attribute.rb
@@ -18,10 +18,9 @@ def initialize(name:, type:, converter:)
               end
     end
 
-    # @param object [Object] target to check and convert type with
     # @return [String, Integer, Boolean] type-checked or type-converted object
-    def value(object)
-      v = object.__send__(@name)
+    def value
+      v = yield(@name)
       result = @type.check(v)
       result ? v : @type.convert(v)
     rescue TypeError


Test patch:
diff --git a/test/usecases/conditional_attributes_test.rb b/test/usecases/conditional_attributes_test.rb
index a541170..03569ae 100644
--- a/test/usecases/conditional_attributes_test.rb
+++ b/test/usecases/conditional_attributes_test.rb
@@ -233,6 +233,21 @@ def test_conditional_attributes_with_if_for_nil_attributes
     )
   end
 
+  class UserResource13 < UserResource
+    attributes :name, if: proc { true }
+
+    def name(_object)
+      'From resource method'
+    end
+  end
+
+  def test_conditional_attributes_with_if_and_resource_method
+    assert_equal(
+      '{"id":1,"name":"From resource method"}',
+      UserResource13.new(@user).serialize
+    )
+  end
+
   class Foo
     attr_reader :id, :name
 
@@ -285,4 +300,27 @@ def test_conditional_attributes_with_type
       FooTypedResource.new(foo, params: {flag: false}).serialize
     )
   end
+
+  class FooTypedResource2
+    include Alba::Resource
+
+    attributes id: Integer
+    attributes name: :String, if: proc { params[:flag] }
+
+    def name(_object)
+      'From resource method'
+    end
+  end
+
+  def test_conditional_attributes_with_type_and_resource_method
+    foo = Foo.new(1, 'name')
+    assert_equal(
+      '{"id":1,"name":"From resource method"}',
+      FooTypedResource2.new(foo, params: {flag: true}).serialize
+    )
+    assert_equal(
+      '{"id":1}',
+      FooTypedResource2.new(foo, params: {flag: false}).serialize
+    )
+  end
 end
diff --git a/test/usecases/nested_attribute_test.rb b/test/usecases/nested_attribute_test.rb
index 199bb78..e5fdcc7 100644
--- a/test/usecases/nested_attribute_test.rb
+++ b/test/usecases/nested_attribute_test.rb
@@ -108,4 +108,24 @@ def test_without_key_transformation_cascade
       )
     end
   end
+
+  # TODO: Fix this test
+  # class Bar3Resource
+  #   include Alba::Resource
+  #
+  #   nested_attribute :na do
+  #     attributes :some_value
+  #   end
+  #
+  #   def some_value(_object)
+  #     'From resource method'
+  #   end
+  # end
+  #
+  # def test_nested_attribute_with_resource_method
+  #   assert_equal(
+  #     '{"na":{"some_value":"From resource method"}}',
+  #     Bar3Resource.new(Bar.new('foo')).serialize
+  #   )
+  # end
 end
diff --git a/test/usecases/type_validation_test.rb b/test/usecases/type_validation_test.rb
index 9e449d9..b83898a 100644
--- a/test/usecases/type_validation_test.rb
+++ b/test/usecases/type_validation_test.rb
@@ -69,4 +69,23 @@ def test_it_raises_error_when_type_is_not_supported
       end
     end
   end
+
+  class AnotherUserResource
+    include Alba::Resource
+
+    prefer_resource_method!
+
+    attributes :name, id: [String, true], age: [Integer, true], bio: String, admin: [:Boolean, true], created_at: [String, ->(object) { object.strftime('%F') }]
+
+    def bio(_object)
+      'This is a bio defined in a resource'
+    end
+  end
+
+  def test_it_uses_resource_method
+    assert_equal(
+      '{"name":"Masafumi OKURA","id":"1","age":32,"bio":"This is a bio defined in a resource","admin":false,"created_at":"2020-10-10"}',
+      AnotherUserResource.new(@user).serialize
+    )
+  end
 end