-
-
Save m-o-e/c9c26aee89293b00ba4af235f8123c8f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Lux::Error::ValidationError < Exception | |
getter pointer | |
getter source | |
getter constraint_id | |
getter boundary | |
def initialize(@pointer : String, | |
@source : String, | |
@constraint_id : String, | |
@boundary : String | Int32 | Bool, | |
@message = "is invalid") | |
end | |
end | |
abstract class Lux::Param | |
property name : String = "" | |
property source : String = "" | |
def error!(constraint_id : String, boundary, message) | |
raise Lux::Error::ValidationError.new(@name, @source, constraint_id, boundary, message) | |
end | |
def register(method, path, name, source) | |
@name = name | |
@source = source | |
swagger_fragment = openapi(method, path, name, source) | |
if swagger_fragment | |
Lux::OpenApi.doc.merge!({ "paths" => { "#{path}" => { "#{method}" => swagger_fragment } } }) | |
end | |
self | |
end | |
def openapi(method, path, name, source) | |
nil | |
end | |
end | |
require "./types" | |
abstract class Lux::Endpoint | |
def valid? | |
validation_errors.nil? || validation_errors.empty? | |
end | |
abstract def validation_errors | |
# Have you ever seen a Macro that generates a Macro that generates a Macro? :P | |
macro inherited | |
QUERY_PARAMS = [] of String | |
BODY_PARAMS = [] of String | |
PATH_PARAMS = [] of String | |
@validation_errors : Array(Error::ValidationError)? = nil | |
macro query_param(name, type, required=true, **args) | |
\{% QUERY_PARAMS << name.stringify %} | |
# Init and store Validator | |
\{% if args.empty? %} | |
@q_\{{name}}_param : Lux::\{{type}}Param = Lux::\{{type}}Param.new.register(METHOD, PATH, \{{name.stringify}}, "query") | |
\{% else %} | |
@q_\{{name}}_param : Lux::\{{type}}Param = Lux::\{{type}}Param.new(**\{{args}}).register(METHOD, PATH, \{{name.stringify}}, "query") | |
\{% end %} | |
@q_\{{name}} : \{{type}}? | |
def q_\{{name}}=(value : String) | |
@q_\{{name}} = @q_\{{name}}_param.from_string(value).not_nil! | |
end | |
def q_\{{name}} | |
return @q_\{{name}}.not_nil! unless @q_\{{name}}.nil? # FIXME IN OTHERS not_nil | |
unless query_params.has_key?(\{{name.stringify}}) | |
\{% if required %} | |
raise Lux::Error::ValidationError.new(\{{name.stringify}}, "query", "required", true, "can't be blank") | |
\{% else %} | |
return nil | |
\{% end %} | |
end | |
self.q_\{{name}} = query_params[\{{name.stringify}}] | |
end | |
end | |
macro body_param(name, type, required=true, **args) | |
\{% BODY_PARAMS << name.stringify %} | |
# Init and store Validator | |
\{% if args.empty? %} | |
@b_\{{name}}_param : Lux::\{{type}}Param = Lux::\{{type}}Param.new.register(METHOD, PATH, \{{name.stringify}}, "body") | |
\{% else %} | |
@b_\{{name}}_param : Lux::\{{type}}Param = Lux::\{{type}}Param.new(**\{{args}}).register(METHOD, PATH, \{{name.stringify}}, "body") | |
\{% end %} | |
@b_\{{name}} : \{{type}}? | |
def b_\{{name}}=(value : String) | |
@b_\{{name}} = @b_\{{name}}_param.from_string(value).not_nil! | |
rescue ex : JSON::SerializableError | |
# This happens when deserialization of a complex `body_param` fails. | |
# E.g.: | |
# | |
# body_param foo, Pet | |
# | |
# Will use the following code: | |
# foo = Pet.from_json(body_params["foo"]) | |
# | |
# When such an error bubbles up we make an effort to | |
# turn it into a clean validation error. | |
ex_msg = ex.cause.to_s || "" | |
err_msg = "is invalid" | |
expected_type = "?" # FIXME, put sth useful here | |
if ex.cause.try &.to_s.starts_with? "Expected" | |
expected_type = /Expected (\w+)/.match(ex_msg).try &.[1] || "?" | |
raise Lux::Error::ValidationError.new("/\{{name}}/#{ex.attribute}", "body", "format", expected_type, "must be a #{expected_type}") | |
elsif ex.message.try &.starts_with? "Missing" | |
missing_attr_name = /Missing JSON attribute: ([^ \n]+)/.match(ex.message.not_nil!).try &.[1] || "?" | |
raise Lux::Error::ValidationError.new("/\{{name}}/#{missing_attr_name}", "body", "required", true, "can't be blank") | |
end | |
raise Lux::Error::ValidationError.new("/\{{name}}", "body", "format", expected_type, err_msg) | |
end | |
def b_\{{name}} | |
return @b_\{{name}}.not_nil! unless @b_\{{name}}.nil? | |
unless body_params.has_key?(\{{name.stringify}}) | |
\{% if required %} | |
raise Lux::Error::ValidationError.new(\{{name.stringify}}, "body", "required", true, "can't be blank") | |
\{% else %} | |
return nil | |
\{% end %} | |
end | |
self.b_\{{name}} = body_params[\{{name.stringify}}] | |
end | |
end | |
macro path_param(name, type, required=true, **args) | |
\{% PATH_PARAMS << name.stringify %} | |
\{% if args.empty? %} | |
@p_\{{name}}_param : Lux::\{{type}}Param = Lux::\{{type}}Param.new.register(METHOD, PATH, \{{name.stringify}}, "path") | |
\{% else %} | |
@p_\{{name}}_param : Lux::\{{type}}Param = Lux::\{{type}}Param.new(**\{{args}}).register(METHOD, PATH, \{{name.stringify}}, "path") | |
\{% end %} | |
@p_\{{name}} : \{{type}}? | |
def p_\{{name}}=(value : String) | |
@p_\{{name}} = @p_\{{name}}_param.from_string(value).not_nil! | |
end | |
def p_\{{name}} | |
return @p_\{{name}}.not_nil! unless @p_\{{name}}.nil? | |
unless path_params.has_key?(\{{name.stringify}}) | |
\{% if required %} | |
raise Lux::Error::ValidationError.new(\{{name.stringify}}, "path", "required", true, "can't be blank") | |
\{% else %} | |
return nil | |
\{% end %} | |
end | |
self.p_\{{name}} = path_params[\{{name.stringify}}] | |
end | |
end | |
macro finished | |
def validation_errors | |
if cached_errors = @validation_errors | |
return (cached_errors.empty? ? nil : cached_errors) | |
end | |
errors = [] of Error::ValidationError | |
\{% for param_name in QUERY_PARAMS %} | |
begin | |
q_\{{param_name.id}} | |
rescue ex : Error::ValidationError | |
errors << ex | |
end | |
\{% end %} | |
\{% for param_name in BODY_PARAMS %} | |
begin | |
b_\{{param_name.id}} | |
rescue ex : Error::ValidationError | |
errors << ex | |
end | |
\{% end %} | |
\{% for param_name in PATH_PARAMS %} | |
begin | |
p_\{{param_name.id}} | |
rescue ex : Error::ValidationError | |
errors << ex | |
end | |
\{% end %} | |
@validation_errors = errors | |
return (errors.empty? ? nil : errors) | |
end | |
end | |
end | |
end | |
# Default renderer for validation errors | |
Lux.render(Array(Lux::Error::ValidationError)) do |v, c| | |
# Query or path parameter violation is a HTTP 400 | |
# Body parameter violation is a HTTP 422 | |
status = v.any?{ |ve| %w[path query].includes?(ve.source) } ? HTTP::Status::BAD_REQUEST : HTTP::Status::UNPROCESSABLE_ENTITY | |
c.format(MediaType::JSON_API, MediaType::JSON) do | |
c.respond(status, | |
{ errors: v.map { |ve| { | |
code: "constraint_violation.#{ve.source}_parameter.#{ve.constraint_id}", | |
title: ve.message, | |
# source: ve.source == "query" ? { parameter: ve.pointer } : { pointer: ve.pointer } | |
source: case ve.source | |
when "query" | |
{ parameter: ve.pointer } | |
when "path" | |
{ token: ve.pointer } | |
else | |
{ pointer: ve.pointer } | |
end | |
} | |
} | |
}.to_json, | |
MediaType::JSON_API | |
) | |
end | |
c.format("*/*", MediaType::TEXT) do | |
body = String.build do |s| | |
s << "ERROR: " | |
s << status.to_s | |
s << "\n\n" | |
v.each do |v_err| | |
s << v_err.source | |
s << " parameter '" | |
s << v_err.pointer | |
s << "' " | |
s << v_err | |
s << "\n" | |
end | |
s << "\n" | |
end | |
c.respond(HTTP::Status::UNPROCESSABLE_ENTITY, body, "text/plain") | |
end | |
end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment