Repository: okuramasafumi/alba (Ruby)

Issue:
Different style of collection serialization
Alba currently serializes a collection into an Array:

```ruby
class User
  attr_reader :id

  def initialize(id)
    @id = id
  end
end

user1 = User.new(1)
user2 = User.new(2)

class UserResource
  include Alba::Resource
  attributes :id
end

UserResource.new([user1, user2]).serializable_hash
# => [{:id=>1}, {:id=>2}]
```

However, sometimes we want to serialize a collection into a Hash with a certain key. The interface will be like:

```ruby
class UserResource
  include Alba::Resource

  collection_key :id

  attributes :id
end
```

With `collection_key`, we can specify the key for the Hash. Now `UserResource` will serialize a collection differently.

```ruby
UserResource.new([user1, user2]).serializable_hash
# => {'1' => {:id=>1}, '2' =>  {:id=>2}}
```

This way, Alba provides more flexible way to deal with collections.


Discussion:
Was there an issue about PR#119? I' think that PR should close this.
Oh I closed that because `to_h` with block is not supported in Ruby 2.5 and 2.6, and at that time Alba supported them.
Now that Alba supports Ruby 2.7 and above that implements `to_h` with block, it's worth salvaging the code.



Patch:
diff --git a/README.md b/README.md
index 5e4b7b9..ebd34d5 100644
--- a/README.md
+++ b/README.md
@@ -823,6 +823,37 @@ UserResource.new(user).serialize
 
 Note that this feature is experimental and interfaces are subject to change.
 
+### Collection serialization into Hash
+
+Sometimes we want to serialize a collection into a Hash, not an Array. It's possible with Alba.
+
+```ruby
+class User
+  attr_reader :id, :name
+  def initialize(id, name)
+    @id, @name = id, name
+  end
+end
+
+class UserResource
+  include Alba::Resource
+
+  collection_key :id # This line is important
+
+  attributes :id, :name
+end
+
+user1 = User.new(1, 'John')
+user2 = User.new(2, 'Masafumi')
+
+UserResource.new([user1, user2]).serialize
+# => '{"1":{"id":1,"name":"John"},"2":{"id":2,"name":"Masafumi"}}'
+```
+
+In the snippet above, `collection_key :id` specifies the key used for the key of the collection hash. In this example it's `:id`.
+
+Make sure that collection key is unique for the collection.
+
 ### Layout
 
 Sometimes we'd like to serialize JSON into a template. In other words, we need some structure OUTSIDE OF serialized JSON. IN HTML world, we call it a "layout".
diff --git a/lib/alba/resource.rb b/lib/alba/resource.rb
index 80629f9..34b1cfb 100644
--- a/lib/alba/resource.rb
+++ b/lib/alba/resource.rb
@@ -7,7 +7,7 @@ module Alba
   module Resource
     # @!parse include InstanceMethods
     # @!parse extend ClassMethods
-    DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_type: :none, _transforming_root_key: false, _on_error: nil, _on_nil: nil, _layout: nil}.freeze # rubocop:disable Layout/LineLength
+    DSLS = {_attributes: {}, _key: nil, _key_for_collection: nil, _meta: nil, _transform_type: :none, _transforming_root_key: false, _on_error: nil, _on_nil: nil, _layout: nil, _collection_key: nil}.freeze # rubocop:disable Layout/LineLength
     private_constant :DSLS
 
     WITHIN_DEFAULT = Object.new.freeze
@@ -65,11 +65,7 @@ def serialize(root_key: nil, meta: {})
       #
       # @return [Hash]
       def serializable_hash
-        if collection?
-          @object.each_with_object([], &collection_converter)
-        else
-          converter.call(@object)
-        end
+        collection? ? serializable_hash_for_collection : converter.call(@object)
       end
       alias to_h serializable_hash
 
@@ -112,6 +108,14 @@ def hash_with_metadata(hash, meta)
         hash
       end
 
+      def serializable_hash_for_collection
+        if @_collection_key
+          @object.to_h { |item| [item.public_send(@_collection_key).to_s, converter.call(item)] }
+        else
+          @object.each_with_object([], &collection_converter)
+        end
+      end
+
       def fetch_key
         collection? ? _key_for_collection : _key
       end
@@ -432,6 +436,13 @@ def transform_keys(type, root: true)
         @_transforming_root_key = root
       end
 
+      # Sets key for collection serialization
+      #
+      # @param key [String, Symbol]
+      def collection_key(key)
+        @_collection_key = key.to_sym
+      end
+
       # Set error handler
       # If this is set it's used as a error handler overriding global one
       #


Test patch:
diff --git a/test/usecases/collection_test.rb b/test/usecases/collection_test.rb
index f60f615..354fe76 100644
--- a/test/usecases/collection_test.rb
+++ b/test/usecases/collection_test.rb
@@ -46,14 +46,14 @@ def setup
     @user2.articles << article2
   end
 
-  def test_array
+  def test_array_with_key
     assert_equal(
       '{"users":[{"id":1,"articles":[{"title":"Hello World!"}]},{"id":2,"articles":[{"title":"Super nice"}]}]}',
       UserResource.new([@user1, @user2]).serialize(root_key: :users)
     )
   end
 
-  def test_array_no_with_arg
+  def test_array_without_key
     assert_equal(
       '[{"id":1,"articles":[{"title":"Hello World!"}]},{"id":2,"articles":[{"title":"Super nice"}]}]',
       UserResource.new([@user1, @user2]).serialize
@@ -74,4 +74,15 @@ def test_array_using_attribute_methods
       UserResourceWithAdditionalAttribute.new([@user1, @user2]).serialize
     )
   end
+
+  class HashUserResource < UserResource
+    collection_key :id
+  end
+
+  def test_array_with_collection_key
+    assert_equal(
+      '{"1":{"id":1,"articles":[{"title":"Hello World!"}]},"2":{"id":2,"articles":[{"title":"Super nice"}]}}',
+      HashUserResource.new([@user1, @user2]).serialize
+    )
+  end
 end