6772312ea7
The primary license of the project is changing to: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later with some specific files to be licensed under the one of two licenses: CC0-1.0 LGPL-3.0-or-later This commit is one of the many steps to relicense the entire codebase. Documentation granting permission for this relicensing (from all past contributors who hold copyrights) is on file with Software Freedom Conservancy, Inc.
140 lines
4.3 KiB
Ruby
140 lines
4.3 KiB
Ruby
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
|
# Provide a declarative json validation and error responses for the rails 'render' method in controllers
|
|
#
|
|
# The return value of the block you pass into #when_valid should look like {status: code, json: data}
|
|
#
|
|
# Concerns of this lib are:
|
|
# * Validate the params, only running a block when valid
|
|
# * Provide a declarative system for request parameter requirements
|
|
# * Respond with proper codes and error messages for everything
|
|
|
|
class JsonResp
|
|
|
|
attr_accessor :errors
|
|
|
|
def initialize(params, &block)
|
|
@params = params
|
|
validation = JsonResp::Validation.new(params)
|
|
validation.instance_exec(params, &block)
|
|
@errors = validation.errors
|
|
return self
|
|
end
|
|
|
|
def when_valid(&block)
|
|
return {status: 422, json: {errors: @errors}} if @errors.any?
|
|
begin
|
|
@response = block.call(@params)
|
|
rescue Exception => e
|
|
@response = {status: 500, json: {error: "We're sorry, but something went wrong. We've been notified about this issue."}}
|
|
puts e
|
|
puts e.backtrace.first(10)
|
|
end
|
|
return @response
|
|
end
|
|
|
|
|
|
# Validation of a set of request parameters
|
|
class Validation
|
|
|
|
attr_accessor :errors, :params
|
|
|
|
def initialize(params)
|
|
@params = params
|
|
@errors = []
|
|
return self
|
|
end
|
|
|
|
def requires(*keys)
|
|
@errors.concat keys.select{|k| @params[k].blank? }.map{|k| "#{k} required"}
|
|
return Param.new(keys, @errors, @params)
|
|
end
|
|
|
|
def requires_either(key1, key2)
|
|
error_message = "#{key1} or #{key2} is required"
|
|
if @params[key1].blank? && @params[key2].blank?
|
|
@errors << error_message
|
|
else
|
|
@errors.concat [key1, key2].select{|k| @params[k].blank? }.map{|k| "#{k} required"}
|
|
end
|
|
return Param.new([key1, key2], @errors, @params)
|
|
end
|
|
|
|
def optional(*keys)
|
|
keys_to_check = keys.select{|k| @params[k].present?}
|
|
return Param.new(keys_to_check, @errors, @params)
|
|
end
|
|
|
|
end
|
|
|
|
class Param
|
|
# param validation methods
|
|
# To make more validators, you can extend this class
|
|
# All methods here are no-ops if the key was optional and is not present
|
|
|
|
attr_accessor :keys, :errors, :params
|
|
|
|
def initialize(keys, errors, params)
|
|
@keys = keys.reject{|k| params[k].nil? }
|
|
@errors = errors
|
|
@params = params
|
|
end
|
|
|
|
def as_string
|
|
@errors.concat @keys.reject{|k| @params[k].is_a?(String)}.map{|k| "#{k} must be a string"}
|
|
return self
|
|
end
|
|
|
|
def as_int
|
|
@errors.concat @keys
|
|
.reject{|k| @params[k].is_a?(Integer) || @params[k].to_i.to_s == @params[k]}
|
|
.map{|k| "#{k} must be an integer"}
|
|
return self
|
|
end
|
|
|
|
def with_format(regex)
|
|
@errors.concat @keys.reject{|k| @params[k] =~ regex}.map{|k| "#{k} must match: #{regex}"}
|
|
return self
|
|
end
|
|
|
|
def one_of(*vals)
|
|
@errors.concat @keys.reject{|k| vals.include?(@params[k])}.map{|k| "#{k} must be one of: #{vals.join(", ")}"}
|
|
return self
|
|
end
|
|
|
|
def nested(&block)
|
|
@errors.concat @keys.map{|k| Validation.new(@params[k]).instance_exec(@params, &block).errors}.flatten
|
|
return self
|
|
end
|
|
|
|
def as_array
|
|
@errors.concat @keys.reject{|k| @params[k].is_a?(Array)}.map{|k| "#{k} must be an array"}
|
|
end
|
|
|
|
def array_of(&block)
|
|
@errors.concat @keys.reject{|k| @params[k].is_a?(Array)}.map{|k| "#{k} must be an array"}
|
|
@errors.concat @keys.map{|k| @params[k].map{|h| Validation.new(h).instance_exec(@params, &block).errors}}.flatten
|
|
return self
|
|
end
|
|
|
|
def as_date
|
|
with_format /\d\d\d\d-\d\d-\d\d/
|
|
@errors.concat @keys.map{|k| [k].concat @params[k].split('-').map(&:to_i)}
|
|
.reject{|key, year, month, day| year.present? && year > 1000 && year < 3000 && month.present? && month > 0 && month < 13 && day.present? && day > 0 && day < 32}
|
|
.map{|k, _,_,_| "#{k} must be a valid date"}
|
|
end
|
|
|
|
def min(n)
|
|
@errors.concat @keys.reject{|k| @params[k] >= n}.map{|k| "#{k} must be at least #{n}"}
|
|
return self
|
|
end
|
|
|
|
def max(n)
|
|
@errors.concat @keys.reject{|k| @params[k] <= n}.map{|k| "#{k} must be less than #{n + 1}"}
|
|
return self
|
|
end
|
|
|
|
# TODO min_len, max_len, as_float, as_currency, as_time, as_datetime
|
|
# TODO return err resp on unrecognized params
|
|
|
|
end
|
|
end
|