Repository: okuramasafumi/alba (Ruby)

Issue:
Change/Override params in associations
Would it be possible to allow something like this?

```ruby
class FooJson
  include Alba::Resource
  one :bar, resource: BarJson, params: { some: "other_value" }
end

FooJson.new(data, params: { some: "value" }).serialize
```

Thanks!


Discussion:
@lukaspili Hi, it's currently not possible but if there're some usecases I'd like to support it.
Could you share your usecase? For example, what kind of JSON output you want with given object?
Thanks for the quick reply!
My concrete use case is that I want to expose different attributes depending on permissions.
This is a simple example with one nesting, but I have some use cases where the `scope` params is passed down to several nested associations.

```ruby 
class UserJson
  include Alba::Resource

  attributes :email_address, if: proc { params[:scope] == "session" }
end

class ContractJson
  include Alba::Resource

  one :creator, resource: UserJson # apply session scope 
  one :counter_party, resource: UserJson # apply anon scope 
end
```
One way to achieve this goal would be to use inheritance, for example:

```ruby
class UserJson
  include Alba::Resource

  attributes :email_address
end

class AnonUserJson < UserJson
  def attributes
    super.reject {|k, v| k == :email_address}
  end
end
```

`AnonUserJson` is a customized version of `UserJson` that rejects some attributes.
Yes - that's a good suggestion. However inheritance can quickly show its limits compared to params when `scope` is not a single value but a list or a composition of multiple `scopeA`, `scopeB`
True. Would you implement this feature on your own? If you will I can help.
If not, then don't worry, I'll implement it.
Sorry - I won't have time to tackle it. Thanks in advance if you do it, but if not I understand!
Hi @lukaspili , at #227 I've implemented this feature. I added some descriptions to README so please could you check it and see if it's good for you?
Hi @okuramasafumi sorry for the late reply. It looks good :) Thank you!



Patch:
diff --git a/README.md b/README.md
index f162017..260a465 100644
--- a/README.md
+++ b/README.md
@@ -197,6 +197,23 @@ class UserResource
 end
 ```
 
+#### Params
+
+You can pass a Hash to the resource for internal use. It can be used as "flags" to control attribute content.
+
+```ruby
+class UserResource
+  include Alba::Resource
+  attribute :name do |user|
+    params[:upcase] ? user.name.upcase : user.name
+  end
+end
+
+user = User.new(1, 'Masa', 'test@example.com')
+UserResource.new(user).serialize # => "{\"name\":\"foo\"}"
+UserResource.new(user, params: {upcase: true}).serialize # => "{\"name\":\"FOO\"}"
+```
+
 ### Serialization with associations
 
 Associations can be defined using the `association` macro, which is also aliased as `one`, `many`, `has_one`, and `has_many` for convenience.
@@ -379,6 +396,49 @@ end
 
 Note that using a Proc slows down serialization if there are too `many` associated objects.
 
+#### Params override
+
+Associations can override params. This is useful when associations are deeply nested.
+
+```ruby
+class BazResource
+  include Alba::Resource
+
+  attributes :data
+  attributes :secret, if: proc { params[:expose_secret] }
+end
+
+class BarResource
+  include Alba::Resource
+
+  one :baz, resource: BazResource
+end
+
+class FooResource
+  include Alba::Resource
+
+  root_key :foo
+
+  one :bar, resource: BarResource
+end
+
+class FooResourceWithParamsOverride
+  include Alba::Resource
+
+  root_key :foo
+
+  one :bar, resource: BarResource, params: { expose_secret: false }
+end
+
+Baz = Struct.new(:data, :secret)
+Bar = Struct.new(:baz)
+Foo = Struct.new(:bar)
+
+foo = Foo.new(Bar.new(Baz.new(1, 'secret')))
+FooResource.new(foo, params: {expose_secret: true}).serialize # => '{"foo":{"bar":{"baz":{"data":1,"secret":"secret"}}}}'
+FooResourceWithParamsOverride.new(foo, params: {expose_secret: true}).serialize # => '{"foo":{"bar":{"baz":{"data":1}}}}'
+```
+
 ### Inline definition with `Alba.serialize`
 
 `Alba.serialize` method is a shortcut to define everything inline.
diff --git a/lib/alba/association.rb b/lib/alba/association.rb
index e169a1a..9ff0143 100644
--- a/lib/alba/association.rb
+++ b/lib/alba/association.rb
@@ -11,12 +11,14 @@ class << self
     # @param name [Symbol, String] name of the method to fetch association
     # @param condition [Proc, nil] a proc filtering data
     # @param resource [Class<Alba::Resource>, nil] a resource class for the association
+    # @param params [Hash] params override for the association
     # @param nesting [String] a namespace where source class is inferred with
     # @param block [Block] used to define resource when resource arg is absent
-    def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
+    def initialize(name:, condition: nil, resource: nil, params: {}, nesting: nil, &block)
       @name = name
       @condition = condition
       @resource = resource
+      @params = params
       return if @resource
 
       assign_resource(nesting, block)
@@ -29,6 +31,7 @@ def initialize(name:, condition: nil, resource: nil, nesting: nil, &block)
     # @param params [Hash] user-given Hash for arbitrary data
     # @return [Hash]
     def to_h(target, within: nil, params: {})
+      params = params.merge(@params) unless @params.empty?
       @object = 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 dcc0f1f..bb1c9d1 100644
--- a/lib/alba/resource.rb
+++ b/lib/alba/resource.rb
@@ -36,7 +36,7 @@ module InstanceMethods
       # @param within [Object, nil, false, true] determines what associations to be serialized. If not set, it serializes all associations.
       def initialize(object, params: {}, within: WITHIN_DEFAULT)
         @object = object
-        @params = params.freeze
+        @params = params
         @within = within
         @method_existence = {} # Cache for `respond_to?` result
         DSLS.each_key { |name| instance_variable_set("@#{name}", self.class.__send__(name)) }
@@ -330,14 +330,15 @@ def attribute(name, **options, &block)
       # @param condition [Proc, nil] a Proc to modify the association
       # @param resource [Class<Alba::Resource>, String, Proc, nil] representing resource for this association
       # @param key [String, Symbol, nil] used as key when given
+      # @param params [Hash] params override for the association
       # @param options [Hash<Symbol, Proc>]
       # @option options [Proc] if a condition to decide if this association should be serialized
       # @param block [Block]
       # @return [void]
       # @see Alba::Association#initialize
-      def association(name, condition = nil, resource: nil, key: nil, **options, &block)
+      def association(name, condition = nil, resource: nil, key: nil, params: {}, **options, &block)
         nesting = self.name.nil? ? nil : self.name.rpartition('::').first.tap {|n| n.empty? ? nil : n}
-        assoc = Association.new(name: name, condition: condition, resource: resource, nesting: nesting, &block)
+        assoc = Association.new(name: name, condition: condition, resource: resource, params: params, nesting: nesting, &block)
         @_attributes[key&.to_sym || name.to_sym] = options[:if] ? ConditionalAttribute.new(body: assoc, condition: options[:if]) : assoc
       end
       alias one association


Test patch:
diff --git a/test/usecases/params_test.rb b/test/usecases/params_test.rb
index 996f4c5..31ee299 100644
--- a/test/usecases/params_test.rb
+++ b/test/usecases/params_test.rb
@@ -78,16 +78,6 @@ class UserResource2
          resource: ArticleResource
   end
 
-  class UserResource3 < UserResource2
-    cond = proc do |articles, params|
-      params[:foo] = true
-      params[:foo] ? [] : articles
-    end
-    many :articles,
-         cond,
-         resource: ArticleResource
-  end
-
   def setup
     Alba.backend = nil
 
@@ -119,7 +109,48 @@ def test_params_works_in_one_and_many
     )
   end
 
-  def test_params_cannot_be_modified
-    assert_raises(FrozenError) { UserResource3.new(@user).serialize }
+  class BazResource
+    include Alba::Resource
+
+    attributes :data
+    attributes :secret, if: proc { params[:expose_secret] }
+  end
+
+  class BarResource
+    include Alba::Resource
+
+    one :baz, resource: BazResource
+  end
+
+  class FooResource
+    include Alba::Resource
+
+    root_key :foo
+
+    one :bar, resource: BarResource
+  end
+
+  class FooResourceWithParamsOverride
+    include Alba::Resource
+
+    root_key :foo
+
+    one :bar, resource: BarResource, params: { expose_secret: false }
+  end
+
+  Baz = Struct.new(:data, :secret)
+  Bar = Struct.new(:baz)
+  Foo = Struct.new(:bar)
+
+  def test_params_goes_down_to_nested_resources
+    foo = Foo.new(Bar.new(Baz.new(1, 'secret')))
+    assert_equal(
+      '{"foo":{"bar":{"baz":{"data":1,"secret":"secret"}}}}',
+      FooResource.new(foo, params: {expose_secret: true}).serialize
+    )
+    assert_equal(
+      '{"foo":{"bar":{"baz":{"data":1}}}}',
+      FooResourceWithParamsOverride.new(foo, params: {expose_secret: true}).serialize
+    )
   end
 end