houdini/lib/json_resp.rb
Bradley M. Kuhn 6772312ea7 Relicense all .rb files under new project license.
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.
2018-03-25 15:10:40 -04:00

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