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

Validate Params #113

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

+ **1.5.0** - _05/23/2018_
- Add the capability to validate params based on documented parameters on endpoints. Invoking
`brainstem_validate_params!` raises an error if unknown parameters are encountered.

+ **1.4.1** - _05/09/2018_
- Add the capability to specify an alternate base application / engine the routes are derived from.
This capability is specific to documemtation generation.
Expand Down
19 changes: 18 additions & 1 deletion lib/brainstem/concerns/controller_dsl.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'brainstem/concerns/inheritable_configuration'
require 'brainstem/params_validator'
require 'active_support/core_ext/object/with_options'

module Brainstem
Expand Down Expand Up @@ -370,7 +371,8 @@ def valid_params_tree(requested_context = action_name.to_sym)
# Lists all valid parameters for the current action. Falls back to the
# valid parameters for the default context.
#
# @params [Symbol] requested_context the context which to look up.
# @params [String, Symbol] (Optional) requested_context the context which to look up.
# @params [String, Symbol] (Optional) root_param_name the param name of the model being changed.
#
# @return [Hash{String => String, Hash] a hash of pairs of param names and
# descriptions or sub-hashes.
Expand All @@ -380,6 +382,21 @@ def brainstem_valid_params(requested_context = action_name.to_sym, root_param_na
end
alias_method :brainstem_valid_params_for, :brainstem_valid_params

#
# Ensures that the parameters passed through to the action are valid.
#
# It raises Brainstem::ValidatorError.new(message, unknown_params, malformed_params) error,
# when params are missing or unknown params are encountered
#
# @params [String, Symbol] (Optional) requested_context the context which to look up.
# @params [String, Symbol] (Optional) root_param_name the param name of the model being changed.
#
def brainstem_validate_params!(requested_context = action_name.to_sym, root_param_name = brainstem_model_name)
input_params = params.with_indifferent_access[brainstem_model_name]
brainstem_params_config = brainstem_valid_params(requested_context, root_param_name)

Brainstem::ParamsValidator.validate!(requested_context, input_params, brainstem_params_config)
end

#
# Lists all incoming param keys that will be rewritten to use a different
Expand Down
10 changes: 10 additions & 0 deletions lib/brainstem/malformed_params.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Brainstem
class MalformedParams < StandardError
attr_reader :malformed_params

def initialize(message = "Malformed Params sighted", malformed_params = [])
@malformed_params = malformed_params
super(message)
end
end
end
92 changes: 92 additions & 0 deletions lib/brainstem/params_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
require 'brainstem/unknown_params'
require 'brainstem/malformed_params'
require 'brainstem/validation_error'

module Brainstem
class ParamsValidator
attr_reader :malformed_params, :unknown_params

def self.validate!(action_name, input_params, valid_params_config)
new(action_name, input_params, valid_params_config).validate!
end

def initialize(action_name, input_params, valid_params_config)
@valid_params_config = valid_params_config
@input_params = sanitize_input_params!(input_params)
@action_name = action_name.to_s

@unknown_params = []
@malformed_params = []
end

def validate!
@input_params.each do |param_key, param_value|
param_data = @valid_params_config[param_key]

if param_data.blank?
@unknown_params << param_key
next
end

param_config = param_data[:_config]
nested_valid_params = param_data.except(:_config)

if param_config[:only].present? && !param_config[:only].map(&:to_s).include?(@action_name)
@unknown_params << param_key
elsif param_config[:recursive].to_s == 'true'
validate_nested_params(param_key, param_config, param_value, @valid_params_config)
elsif parent_param?(param_data)
validate_nested_params(param_key, param_config, param_value, nested_valid_params)
end
end

raise_when_invalid? ? unknown_params_error! : true
end

private

def raise_when_invalid?
@malformed_params.present? || @unknown_params.present?
end

def parent_param?(param_data)
param_data.except(:_config).keys.present?
end

def validate_nested_params(param_key, param_config, value, valid_nested_params)
return value if value.nil?

param_type = param_config[:type]
if param_type == 'hash'
validate_nested_param(param_key, param_type, value, valid_nested_params)
elsif param_type == 'array' && !value.is_a?(Array)
@malformed_params << param_key
else
value.each { |value| validate_nested_param(param_key, param_type, value, valid_nested_params) }
end
end

def validate_nested_param(parent_param_key, parent_param_type, value, valid_nested_params)
begin
self.class.validate!(@action_name, value, valid_nested_params)
rescue Brainstem::ValidationError => e
@unknown_params << { parent_param_key => e.unknown_params }
@malformed_params << { parent_param_key => e.malformed_params }
end
end

def sanitize_input_params!(input_params)
malformed_params_error! unless input_params.is_a?(Hash) && input_params.present?
Copy link
Contributor

@brandonduff brandonduff Jul 18, 2018

Choose a reason for hiding this comment

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

Do we to_h params at some point before this? Otherwise this won't work in Rails 5, since ActionController::Parameters no longer descend from Hash.

EDIT: It looks we call with_indifferent_access on them above. I don't have an environment setup to test that on Rails 5 right now, but I'm not sure if ActionController::Parameters supports that method in 5.


input_params
end

def malformed_params_error!
raise ::Brainstem::ValidationError.new("Input params are malformed")
end

def unknown_params_error!
raise ::Brainstem::ValidationError.new("Invalid params encountered", @unknown_params, @malformed_params)
end
end
end
10 changes: 10 additions & 0 deletions lib/brainstem/unknown_params.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Brainstem
class UnknownParams < StandardError
attr_reader :unknown_params

def initialize(message = "Unidentified Params sighted", unknown_params = [])
@unknown_params = unknown_params
super(message)
end
end
end
12 changes: 12 additions & 0 deletions lib/brainstem/validation_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Brainstem
class ValidationError < StandardError
attr_reader :unknown_params, :malformed_params

def initialize(message = "Invalid params sighted", unknown_params = [], malformed_params = [])
@unknown_params = unknown_params
@malformed_params = malformed_params

super(message)
end
end
end
70 changes: 70 additions & 0 deletions spec/brainstem/concerns/controller_dsl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,76 @@ module Concerns
end
end

describe "#brainstem_validate_params!" do
let(:brainstem_model_name) { "widget" }
let(:input_params) { { widget: { sprocket_parent_id: 5, sprocket_name: 'gears' } } }

before do
stub(subject).brainstem_model_name { brainstem_model_name }
stub.any_instance_of(subject).brainstem_model_name { brainstem_model_name }
stub.any_instance_of(subject).params { input_params }

subject.brainstem_params do
actions :update do
model_params(brainstem_model_name) do |params|
params.valid :sprocket_parent_id, :long,
info: "sprockets[sprocket_parent_id] is not required"

params.valid :sprocket_name, :string,
info: "sprockets[sprocket_name] is required",
required: true
end
end
end
end

it "returns true if params are OK" do
expect(subject.new.brainstem_validate_params!(:update, brainstem_model_name)).to be_truthy
end

context "when parameters are in an invalid format" do
context "with an empty hash" do
let(:input_params) { {} }

it "returns false and says params are missing" do
expect {
subject.new.brainstem_validate_params!(:update, brainstem_model_name)
}.to raise_error(Brainstem::ValidationError)
end
end

context "with a non-hash object" do
let(:input_params) { { widget: [{ foo: "bar" }] } }

it "returns false and says params are missing" do
expect {
subject.new.brainstem_validate_params!(:update, brainstem_model_name)
}.to raise_error(Brainstem::ValidationError)
end
end

context "with nil" do
let(:input_params) { { widget: nil } }

it "returns false and says params are missing" do
expect {
subject.new.brainstem_validate_params!(:update, brainstem_model_name)
}.to raise_error(Brainstem::ValidationError)
end
end
end

context "when there are errors due to unknown params" do
let(:input_params) { { widget: { my_cool_param: "something" } } }

it "lists unknown params" do
expect {
subject.new.brainstem_validate_params!(:update, brainstem_model_name)
}.to raise_error(Brainstem::ValidationError)
end
end
end

describe "#transforms" do
before do
subject.brainstem_params do
Expand Down
Loading