Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controller parameters wrapping feature #89

Merged
merged 5 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions lib/rage/controller/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ def __register_action(action)

activerecord_loaded = defined?(::ActiveRecord)

wrap_parameters_chunk = if __wrap_parameters_key
<<~RUBY
wrap_key = self.class.__wrap_parameters_key
if !@__params.key?(wrap_key) && @__env['CONTENT_TYPE']
wrap_options = self.class.__wrap_parameters_options

wrapped_params = if wrap_options[:include]
@__params.slice(*[wrap_options[:include]].flatten)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this line means. Can we just use *?

Suggested change
@__params.slice(*[wrap_options[:include]].flatten)
@__params.slice(*wrap_options[:include])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I added flatten to support both symbol and array options, like exclude: :param and exclude: [:param1, :param2] but now I think that this is redundant and Rails supports only array options anyway so I'm going to remove flatten

elsif wrap_options[:exclude]
@__params.except(*[wrap_options[:exclude]].flatten)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@__params.except(*[wrap_options[:exclude]].flatten)
@__params.except(*wrap_options[:exclude])

else
@__params
end

@__params = @__params.merge({wrap_key => wrapped_params})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple assign should be much faster.

Suggested change
@__params = @__params.merge({wrap_key => wrapped_params})
@__params[wrap_key] = wrapped_params

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally makes sense 👍

end
RUBY
end

class_eval <<~RUBY, __FILE__, __LINE__ + 1
def __run_#{action}
#{if activerecord_loaded
Expand All @@ -85,6 +104,7 @@ def __run_#{action}
RUBY
end}

#{wrap_parameters_chunk}
#{before_actions_chunk}
#{action}

Expand Down Expand Up @@ -122,6 +142,7 @@ def __run_#{action}

# @private
attr_writer :__before_actions, :__after_actions, :__rescue_handlers
attr_accessor :__wrap_parameters_key, :__wrap_parameters_options

# @private
# pass the variable down to the child; the child will continue to use it until changes need to be made;
Expand All @@ -130,6 +151,8 @@ def inherited(klass)
klass.__before_actions = @__before_actions.freeze
klass.__after_actions = @__after_actions.freeze
klass.__rescue_handlers = @__rescue_handlers.freeze
klass.__wrap_parameters_key = __wrap_parameters_key
klass.__wrap_parameters_options = __wrap_parameters_options
end

# @private
Expand Down Expand Up @@ -277,6 +300,20 @@ def skip_before_action(action_name, only: nil, except: nil)
@__before_actions[i] = action
end

# Initialize controller params wrapping into a nested hash.
# If initialized, params wrapping logic will be added to the controller.
#
# @param key [Symbol] key that the params hash will nested under
# @param options [Hash] wrapping options
# @example
# wrap_parameters :user, include: [:name, :age]
# @example
# wrap_parameters :user, exclude: :address
def wrap_parameters(key, options = {})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific reason you are not using keyword arguments here?

@__wrap_parameters_key = key
@__wrap_parameters_options = options
end

private

# used by `before_action` and `after_action`
Expand Down
319 changes: 319 additions & 0 deletions spec/controller/api/wrap_parameters_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
RSpec.describe RageController::API do
describe 'parameters wrapping logic' do
context 'when parameters wrapper is not declared' do
let(:controller) do
Class.new(RageController::API) do
def index
render json: params
end
end
end

it "doesn't wrap the parameters" do
initial_params = {param: :value}
expected_result = {param: :value}

response = run_action(controller, :index, params: initial_params, env: {'CONTENT_TYPE' => "application/json"})
expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end

context 'when parameters wrapper is declared without options' do
let(:controller) do
Class.new(RageController::API) do
wrap_parameters :root

def index
render json: params
end
end
end

context "and wrapping root doesn't conflict with parameter key" do
context 'and CONTENT_TYPE header is blank' do
it "doesn't wrap the parameters into a nested hash" do
initial_params = {param: :value}
expected_result = {param: :value}

response = run_action(controller, :index, params: initial_params)
expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end

context 'and CONTENT_TYPE header is present' do
it 'wraps the parameters into a nested hash' do
initial_params = {param: :value}
expected_result = {param: :value, root: {param: :value}}

response = run_action(
controller,
:index,
params: initial_params,
env: {'CONTENT_TYPE' => "application/json"}
)

expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end
end

context 'and wrapping root conflicts with parameter key' do
it "doesn't wrap the parameters into a nested hash" do
initial_params = {root: :value, param: :value}
expected_result = {root: :value, param: :value}

response = run_action(controller, :index, params: initial_params, env: {'CONTENT_TYPE' => "application/json"})
expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end
end

context 'when parameters wrapper is declared with :include option' do
context 'and :include option is set as Symbol' do
let(:controller) do
Class.new(RageController::API) do
wrap_parameters :root, include: :param_a

def index
render json: params
end
end
end

it 'wraps the param that is set to be included' do
initial_params = {param_a: :value, param_b: :value}
expected_result = {param_a: :value, param_b: :value, root: {param_a: :value}}

response = run_action(controller, :index, params: initial_params, env: {'CONTENT_TYPE' => "application/json"})
expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end

context 'and :include option is set as Array' do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please also add a test case for when the :include option is not present in the params?

let(:controller) do
Class.new(RageController::API) do
wrap_parameters :root, include: [:param_a, :param_b]

def index
render json: params
end
end
end

it 'wraps the params that are set to be included' do
initial_params = {param_a: :value, param_b: :value, param_c: :value}
expected_result = {
param_a: :value,
param_b: :value,
param_c: :value,
root: {param_a: :value, param_b: :value}
}

response = run_action(controller, :index, params: initial_params, env: {'CONTENT_TYPE' => "application/json"})
expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end
end

context 'when parameters wrapper is declared with :exclude option' do
context 'and :exclude option is set as Symbol' do
let(:controller) do
Class.new(RageController::API) do
wrap_parameters :root, exclude: :param_a

def index
render json: params
end
end
end

it 'wraps the params except the param that is set to be excluded' do
initial_params = {param_a: :value, param_b: :value}
expected_result = {param_a: :value, param_b: :value, root: {param_b: :value}}

response = run_action(controller, :index, params: initial_params, env: {'CONTENT_TYPE' => "application/json"})
expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end

context 'and :exclude option is set as Array' do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please also add a test case for when the :exclude option is not present in the params?

let(:controller) do
Class.new(RageController::API) do
wrap_parameters :root, exclude: [:param_a, :param_b]

def index
render json: params
end
end
end

it 'wraps the params except the params that are set to be excluded' do
initial_params = {param_a: :value, param_b: :value, param_c: :value}
expected_result = {param_a: :value, param_b: :value, param_c: :value, root: {param_c: :value}}

response = run_action(controller, :index, params: initial_params, env: {'CONTENT_TYPE' => "application/json"})
expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end
end

context 'when parameters wrapper is declared with both :exclude and :include options' do
let(:controller) do
Class.new(RageController::API) do
wrap_parameters :root, exclude: :param_a, include: :param_a

def index
render json: params
end
end
end

it 'wraps the params using the :include option' do
initial_params = {param_a: :value, param_b: :value}
expected_result = {param_a: :value, param_b: :value, root: {param_a: :value}}

response = run_action(controller, :index, params: initial_params, env: {'CONTENT_TYPE' => "application/json"})
expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end
end

describe 'controller inheritance' do
let(:grandchild_controller) do
Class.new(child_controller) do
def index
render json: params
end
end
end

context 'when parameters wrapper is declared in parent controller' do
let(:parent_controller) do
Class.new(RageController::API) do
wrap_parameters :parent_root, include: [:parent_param]

def index
render json: params
end
end
end

context 'and parameters wrapper is declared in child controller' do
context 'and child wrapper is declared without options' do
let(:child_controller) do
Class.new(parent_controller) do
wrap_parameters :child_root

def index
render json: params
end
end
end

let(:initial_params) { {parent_param: :value, child_param: :value} }
let(:expected_result) do
{
parent_param: :value,
child_param: :value,
child_root: {parent_param: :value, child_param: :value}
}
end

it 'wraps params of child controller using wrapping key of child controller without options' do
response = run_action(
child_controller,
:index,
params: initial_params,
env: {'CONTENT_TYPE' => "application/json"}
)

expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end

it 'wraps params of grandchild controller using wrapping key of child controller without options' do
response = run_action(
grandchild_controller,
:index,
params: initial_params,
env: {'CONTENT_TYPE' => "application/json"}
)

expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end

context 'and child wrapper is defined with options' do
let(:child_controller) do
Class.new(parent_controller) do
wrap_parameters :child_root, include: [:child_param]

def index
render json: params
end
end
end

let(:initial_params) { {parent_param: :value, child_param: :value} }
let(:expected_result) { {parent_param: :value, child_param: :value, child_root: {child_param: :value}} }

it 'wraps params of child controller using wrapping key and options of child controller' do
response = run_action(
child_controller,
:index,
params: initial_params,
env: { 'CONTENT_TYPE' => "application/json" }
)

expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end

it 'wraps params of grandchild controller using wrapping key and options of child controller' do
response = run_action(
grandchild_controller,
:index,
params: initial_params,
env: {'CONTENT_TYPE' => "application/json"}
)

expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end
end

context 'and parameters wrapper is not declared in child controller' do
let(:child_controller) do
Class.new(parent_controller) do
def index
render json: params
end
end
end

let(:initial_params) { {parent_param: :value, child_param: :value} }
let(:expected_result) { {parent_param: :value, child_param: :value, parent_root: {parent_param: :value}} }

it 'wraps params of child controller using wrapping key and options of parent controller' do
response = run_action(
child_controller,
:index,
params: initial_params,
env: {'CONTENT_TYPE' => "application/json"}
)

expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end

it 'wraps params of grandchild controller using wrapping key and options of parent controller' do
response = run_action(
child_controller,
:index,
params: initial_params,
env: {'CONTENT_TYPE' => "application/json"}
)

expect(response).to match([200, instance_of(Hash), [expected_result.to_json]])
end
end
end
end
end
Loading