Repository: okuramasafumi/alba (Ruby)

Issue:
Support hash access mapped to attributes
## Is your feature request related to a problem? Please describe.

Currently, I'm using [skryukov/typelizer](https://github.com/skryukov/typelizer) to create "Page" type definition from my controller actions. These would be for controllers' actions that don't directly return just resources but other data as well. I would like the ability to be able to pass in a hash into Alba and be able to check if the hash key matches the attributes field instead of having to create an intermediary object out of my hash data. Here is an example:

```ruby
class HomeIndexResource < ApplicationResource
  one :user, resource: UserResource
  many :system_preferences, resource: SystemPreferenceResource
  many :roles, resource: RoleResource
end

class HomeController < ApplicationController
  def index
    render json: HomeIndexResource.new({
      user: Current.user,
      system_preferences: SystemPreference.all,
      roles: Role.where(user_id: Current.user.id)
    })
  end
end
```

Instead, the way that I have been getting around this is converting these hashs into structs, or create a new object before passing the data in.

```ruby
class HomeController < ApplicationController
  def index
    struct = OpenStruct.new(
      user: Current.user,
      system_preferences: Preference.where(user_id: Current.user.id),
      roles: Role.where(user_id: Current.user.id)
    )
    render json: HomeIndexResource.new(struct)
  end
end
```

## Describe the solution you'd like
Ideally, there would be no need to have to convert hash into something else and Alba resources would support being able to pass a hash in directly. Would allow for my dynamically created data to be passed in.

I'm not sure if we would want this to be the default behaviour. I could see adding a configuration option that would add hash support to a resource object. I could see having to call a class method to have the hash key lookup turned on:

```ruby
class HomeIndexResource < ApplicationResource
  hash_hash_access

  one :user, resource: UserResource
  many :system_preferences, resource: SystemPreferenceResource
  many :roles, resource: RoleResource
end
```


Happy to explore other options, also willing to put up a PR if this is an idea that we would want to move forward.


Discussion:
@patvice Hi, thank you for raising a feature request, I confirmed that there is no way to achieve this with Alba right now.
One simple way to make it work is to change how we treat Hash as an argument. Hopefully we don't have to add another DSL but it "just works".
Maybe similar to #397 I'll implement Hash serialization anyway.



Patch:
diff --git a/lib/alba.rb b/lib/alba.rb
index 005f3e6..a66f13a 100644
--- a/lib/alba.rb
+++ b/lib/alba.rb
@@ -83,7 +83,7 @@ def hashify(object = nil, with: :inference, root_key: nil, &block)
     #
     # @api private
     def collection?(object)
-      object.is_a?(Enumerable) && !object.is_a?(Struct) && !object.is_a?(Range)
+      object.is_a?(Enumerable) && !object.is_a?(Struct) && !object.is_a?(Range) && !object.is_a?(Hash)
     end
 
     # Enable inference for key and resource name
diff --git a/lib/alba/association.rb b/lib/alba/association.rb
index d87faf7..5e8a6a0 100644
--- a/lib/alba/association.rb
+++ b/lib/alba/association.rb
@@ -47,7 +47,7 @@ def key_transformation=(type)
     # @return [Hash]
     def to_h(target, within: nil, params: {})
       params = params.merge(@params)
-      object = target.__send__(@name)
+      object = target.is_a?(Hash) ? target.fetch(@name) : target.__send__(@name)
       object = @condition.call(object, params, target) if @condition
       return if object.nil?
 
diff --git a/lib/alba/resource.rb b/lib/alba/resource.rb
index 79a1ec1..62a42fa 100644
--- a/lib/alba/resource.rb
+++ b/lib/alba/resource.rb
@@ -276,12 +276,16 @@ def fetch_attribute_from_object_and_resource(obj, attribute)
       end
 
       def _fetch_attribute_from_object_first(obj, attribute)
+        return obj.fetch(attribute) if obj.is_a?(Hash)
+
         obj.__send__(attribute)
       rescue NoMethodError
         __send__(attribute, obj)
       end
 
       def _fetch_attribute_from_resource_first(obj, attribute)
+        return obj.fetch(attribute) if obj.is_a?(Hash)
+
         if @_resource_methods.include?(attribute)
           __send__(attribute, obj)
         else


Test patch:
diff --git a/test/usecases/hash_serialization_test.rb b/test/usecases/hash_serialization_test.rb
new file mode 100644
index 0000000..c8de23f
--- /dev/null
+++ b/test/usecases/hash_serialization_test.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require_relative '../test_helper'
+
+class HashSerializationTest < Minitest::Test
+  class HashResource
+    include Alba::Resource
+
+    attributes :id, :name
+  end
+
+  def test_simple_hash_serialization
+    hash = {id: 1, name: 'test'}
+    assert_equal(
+      '{"id":1,"name":"test"}',
+      HashResource.new(hash).serialize
+    )
+
+    extra_hash = {id: 1, name: 'test', do_not_display: 42}
+    assert_equal(
+      '{"id":1,"name":"test"}',
+      HashResource.new(extra_hash).serialize
+    )
+  end
+
+  class ManyHashResource
+    include Alba::Resource
+
+    many :items, resource: HashResource
+  end
+
+  def test_hash_serialization_with_many
+    hash = {items: [{id: 1, name: 'test1', do_not_display: 42}, {id: 2, name: 'test2'}]}
+    assert_equal(
+      '{"items":[{"id":1,"name":"test1"},{"id":2,"name":"test2"}]}',
+      ManyHashResource.new(hash).serialize
+    )
+  end
+end