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

Add optional error handling without exception. #71

Open
wants to merge 4 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
/doc/
/rdoc/

.byebug_history

# Environment
/.bundle/
/lib/bundler/man/
Expand Down
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
--color
--order random
--require spec_helper.rb
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

language: ruby
rvm:
- 1.9.3
before_install:
- gem install bundler
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,43 @@ param :y, String
any_of :x, :y
```

### Errors without exception

If you need to consume error with custom business logic (e.g. do not show error one by one but all errors at once). You can do this with this PR.

Set flag to change slightly way how to sinatra param use "bang methods"


```ruby
class AppWithFlash < Sinatra::Base
helpers Sinatra::Param

configure do
set :ruby_best_practice_sinatra_param, true
end
end
```

after seting up this flag will be modified helper method "param", "one_of" and "any_of" and will be created 3 additional methods "param!", "one_of!" and "any_of!" as it's standard in Ruby community methods with bang character in the end will raise error same as for instance save vs save! in ActiveRecord.

So now param helper do not raise exception and halt execution but return value if error happend otherwise just nil value represent "nothing to show".

```ruby
error_for_a = param(:a, String)
```

error_for_a will contain same error message as people are use to to get from standard Sinatra param library.

as bonus we create helper method for collect or errors and remove nil values.

```ruby
errors = errors_for_params do |errors|
param(:a, String)
param(:b, String)
param(:c, String)
end
```

### Exceptions

By default, when a parameter precondition fails, `Sinatra::Param` will `halt 400` with an error message:
Expand Down
170 changes: 149 additions & 21 deletions lib/sinatra/param.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,94 @@ class InvalidParameterError < StandardError
end

def param(name, type, options = {})
name = name.to_s
if (settings.ruby_best_practice_sinatra_param rescue false)
param_without_exception(name, type, options)
else
param_with_exception(name, type, options)
end
end

return unless params.member?(name) or options[:default] or options[:required]
def errors_for_params(&block)
errors ||= []
block.call(errors).compact
end

def param!(name, type, options = {})
param_with_exception(name, type, options)
end

def one_of(*args)
if (settings.ruby_best_practice_sinatra_param rescue false)
one_of_without_exception(*args)
else
one_of_with_exception(*args)
end
end

def one_of!(*args)
one_of_with_exception(*args)
end

def any_of(*args)
if (settings.ruby_best_practice_sinatra_param rescue false)
any_of_without_exception(*args)
else
any_of_with_exception(*args)
end
end

def any_of!(*args)
any_of_with_exception(*args)
end

private

def any_of_without_exception(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
names = args.collect(&:to_s)

return unless names.length >= 2

error = validate_any_of(params, names, options)

error.nil? ? nil : error
end

def any_of_with_exception(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
names = args.collect(&:to_s)

return unless names.length >= 2

begin
params[name] = coerce(params[name], type, options)
params[name] = (options[:default].call if options[:default].respond_to?(:call)) || options[:default] if params[name].nil? and options[:default]
params[name] = options[:transform].to_proc.call(params[name]) if params[name] and options[:transform]
validate!(params[name], options)
validate_any_of!(params, names, options)
rescue InvalidParameterError => exception
if options[:raise] or (settings.raise_sinatra_param_exceptions rescue false)
exception.param, exception.options = name, options
exception.param, exception.options = names, options
raise exception
end

error = exception.to_s

error = "Invalid parameters [#{names.join(', ')}]"
if content_type and content_type.match(mime_type(:json))
error = {message: error, errors: {name => exception.message}}.to_json
error = {message: error, errors: {names => exception.message}}.to_json
end

halt 400, error
end
end

def one_of(*args)
def one_of_without_exception(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
names = args.collect(&:to_s)

return unless names.length >= 2

error = validate_one_of(params, names, options)

error.nil? ? nil : error
end

def one_of_with_exception(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
names = args.collect(&:to_s)

Expand All @@ -60,31 +122,51 @@ def one_of(*args)
end
end

def any_of(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
names = args.collect(&:to_s)
def param_without_exception(name, type, options = {})
name = name.to_s

return unless names.length >= 2
return unless params.member?(name) or options[:default] or options[:required]

begin
validate_any_of!(params, names, options)
params[name] = coerce(params[name], type, options)

params[name] = (options[:default].call if options[:default].respond_to?(:call)) || options[:default] if params[name].nil? and options[:default]
params[name] = options[:transform].to_proc.call(params[name]) if params[name] and options[:transform]

errors = validate(params[name], options)
rescue InvalidParameterError => exception
errors = [exception.message]
end

errors && errors.empty? ? nil : { name => errors }
end

def param_with_exception(name, type, options = {})
name = name.to_s

return unless params.member?(name) or options[:default] or options[:required]

begin
params[name] = coerce(params[name], type, options)
params[name] = (options[:default].call if options[:default].respond_to?(:call)) || options[:default] if params[name].nil? and options[:default]
params[name] = options[:transform].to_proc.call(params[name]) if params[name] and options[:transform]
validate!(params[name], options)
rescue InvalidParameterError => exception
if options[:raise] or (settings.raise_sinatra_param_exceptions rescue false)
exception.param, exception.options = names, options
exception.param, exception.options = name, options
raise exception
end

error = "Invalid parameters [#{names.join(', ')}]"
error = exception.to_s

if content_type and content_type.match(mime_type(:json))
error = {message: error, errors: {names => exception.message}}.to_json
error = {message: error, errors: {name => exception.message}}.to_json
end

halt 400, error
end
end

private

def coerce(param, type, options = {})
begin
return nil if param.nil?
Expand Down Expand Up @@ -142,6 +224,44 @@ def validate!(param, options)
end
end

def validate(param, options)
options.each_with_object([]) do |(key, value), errors|
case key
when :required
errors << "Parameter is required" if value && param.nil?
when :blank
errors << "Parameter cannot be blank" if !value && case param
when String
!(/\S/ === param)
when Array, Hash
param.empty?
else
param.nil?
end
when :format
errors << "Parameter must be a string if using the format validation" unless param.kind_of?(String)
errors << "Parameter must match format #{value}" unless param =~ value
when :is
errors << "Parameter must be #{value}" unless param === value
when :in, :within, :range
errors << "Parameter must be within #{value}" unless param.nil? || case value
when Range
value.include?(param)
else
Array(value).include?(param)
end
when :min
errors << "Parameter cannot be less than #{value}" unless param.nil? || value <= param
when :max
errors << "Parameter cannot be greater than #{value}" unless param.nil? || value >= param
when :min_length
errors << "Parameter cannot have length less than #{value}" unless param.nil? || value <= param.length
when :max_length
errors << "Parameter cannot have length greater than #{value}" unless param.nil? || value >= param.length
end
end
end

def validate_one_of!(params, names, options)
raise InvalidParameterError, "Only one of [#{names.join(', ')}] is allowed" if names.count{|name| present?(params[name])} > 1
end
Expand All @@ -150,6 +270,14 @@ def validate_any_of!(params, names, options)
raise InvalidParameterError, "One of parameters [#{names.join(', ')}] is required" if names.count{|name| present?(params[name])} < 1
end

def validate_one_of(params, names, options)
return "Only one of [#{names.join(', ')}] is allowed" if names.count{|name| present?(params[name])} > 1
end

def validate_any_of(params, names, options)
return "One of parameters [#{names.join(', ')}] is required" if names.count{|name| present?(params[name])} < 1
end

# ActiveSupport #present? and #blank? without patching Object
def present?(object)
!blank?(object)
Expand Down
1 change: 1 addition & 0 deletions sinatra-param.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Gem::Specification.new do |s|
s.add_dependency "sinatra", "~> 1.3"

s.add_development_dependency "rake"
s.add_development_dependency "rack"
s.add_development_dependency "rspec"
s.add_development_dependency "rack-test"
s.add_development_dependency "simplecov"
Expand Down
12 changes: 10 additions & 2 deletions spec/dummy/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
class App < Sinatra::Base
helpers Sinatra::Param

set :show_exceptions, false
set :raise_errors, true
configure do
set :show_exceptions, false
set :raise_errors, true
end

before do
content_type :json
Expand Down Expand Up @@ -238,6 +240,12 @@ class App < Sinatra::Base
params.to_json
end

get '/flash/validations/required' do
param :arg, Integer, required: true, raise: true, min: 12

expect(flash.keys).to be('')
end

get '/raise/one_of/3' do
param :a, String
param :b, String
Expand Down
Loading