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 all 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
38 changes: 38 additions & 0 deletions lib/rage/controller/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,23 @@ 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].any?
@__params.slice(*wrap_options[:include])
else
params_to_exclude_by_default = %i[action controller]
@__params.except(*(wrap_options[:exclude] + params_to_exclude_by_default))
end

@__params[wrap_key] = wrapped_params
end
RUBY
end

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

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

Expand Down Expand Up @@ -122,6 +140,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 +149,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 +298,23 @@ 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.
# Params get wrapped only if the CONTENT_TYPE header is present and params hash doesn't contain a param that
# has the same name as the wrapper key.
#
# @param key [Symbol] key that the wrapped params hash will nested under
# @param include [Array] array of params that should be included to the wrapped params hash
# @param exclude [Array] array of params that should be excluded from the wrapped params hash
# @example
# wrap_parameters :user, include: %i[name age]
# @example
# wrap_parameters :user, exclude: %i[address]
def wrap_parameters(key, include: [], exclude: [])
@__wrap_parameters_key = key
@__wrap_parameters_options = {include:, exclude:}
end

private

# used by `before_action` and `after_action`
Expand Down
308 changes: 308 additions & 0 deletions spec/controller/api/wrap_parameters_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
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 without the reserved params' do
initial_params = {param: :value, action: :action, controller: :controller}
expected_result = {param: :value, action: :action, controller: :controller, 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, action: :action, controller: :controller}
expected_result = {root: :value, param: :value, action: :action, controller: :controller}

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
let(:controller) do
Class.new(RageController::API) do
wrap_parameters :root, include: %i[param_a param_b]

def index
render json: params
end
end
end

context 'and params to include are present in request' do
it 'wraps the params that are set to be included' do
initial_params = {param_a: :value, param_b: :value, param_c: :value, action: :action, controller: :controller}
expected_result = {
param_a: :value,
param_b: :value,
param_c: :value,
action: :action,
controller: :controller,
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

context "and params to include aren't present in request" do
it 'adds empty hash under wrapping key to params' do
initial_params = {param_c: :value, action: :action, controller: :controller}
expected_result = {param_c: :value, action: :action, controller: :controller, root: {}}

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
let(:controller) do
Class.new(RageController::API) do
wrap_parameters :root, exclude: %i[param_a param_b]

def index
render json: params
end
end
end

context 'and params to exclude are present in request' do
it 'wraps the params except those that are set to be excluded and those that need to be excluded by default' do
initial_params = {param_a: :value, param_b: :value, param_c: :value, action: :action, controller: :controller}
expected_result = {
param_a: :value,
param_b: :value,
param_c: :value,
action: :action,
controller: :controller,
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

context "and params to exclude aren't present in request" do
it 'wraps the params except those that need to be excluded by default ' do
initial_params = {param_c: :value, action: :action, controller: :controller}
expected_result = {param_c: :value, action: :action, controller: :controller, 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: %i[param_a], include: %i[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

context '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: %i[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: %i[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
end
Loading