Skip to content

Instantly share code, notes, and snippets.

@m-o-e

m-o-e/params.cr Secret

Created April 10, 2022 19:16
Show Gist options
  • Save m-o-e/c9c26aee89293b00ba4af235f8123c8f to your computer and use it in GitHub Desktop.
Save m-o-e/c9c26aee89293b00ba4af235f8123c8f to your computer and use it in GitHub Desktop.
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