kemal/src/kemal/param_parser.cr

112 lines
3.1 KiB
Crystal

module Kemal
# Parses the request contents including query_params and body
# and converts them into a params hash which you can use within
# the environment context.
class ParamParser
URL_ENCODED_FORM = "application/x-www-form-urlencoded"
APPLICATION_JSON = "application/json"
MULTIPART_FORM = "multipart/form-data"
PARTS = %w(url query body json files)
# :nodoc:
alias AllParamTypes = Nil | String | Int64 | Float64 | Bool | Hash(String, JSON::Any) | Array(JSON::Any)
getter files
def initialize(@request : HTTP::Request, @url : Hash(String, String) = {} of String => String)
@query = HTTP::Params.new({} of String => Array(String))
@body = HTTP::Params.new({} of String => Array(String))
@json = {} of String => AllParamTypes
@files = {} of String => FileUpload
@url_parsed = false
@query_parsed = false
@body_parsed = false
@json_parsed = false
@files_parsed = false
end
private def unescape_url_param(value : String)
value.empty? ? value : URI.decode(value)
rescue
value
end
{% for method in PARTS %}
def {{method.id}}
# check memoization
return @{{method.id}} if @{{method.id}}_parsed
parse_{{method.id}}
# memoize
@{{method.id}}_parsed = true
@{{method.id}}
end
{% end %}
private def parse_body
content_type = @request.headers["Content-Type"]?
return unless content_type
if content_type.try(&.starts_with?(URL_ENCODED_FORM))
@body = parse_part(@request.body)
return
end
if content_type.try(&.starts_with?(MULTIPART_FORM))
parse_files
end
end
private def parse_query
@query = parse_part(@request.query)
end
private def parse_url
@url.each { |key, value| @url[key] = unescape_url_param(value) }
end
private def parse_files
return if @files_parsed
HTTP::FormData.parse(@request) do |upload|
next unless upload
filename = upload.filename
if !filename.nil?
@files[upload.name] = FileUpload.new(upload)
else
@body.add(upload.name, upload.body.gets_to_end)
end
end
@files_parsed = true
end
# Parses JSON request body if Content-Type is `application/json`.
#
# - If request body is a JSON `Hash` then all the params are parsed and added into `params`.
# - If request body is a JSON `Array` it's added into `params` as `_json` and can be accessed like `params["_json"]`.
private def parse_json
return unless @request.body && @request.headers["Content-Type"]?.try(&.starts_with?(APPLICATION_JSON))
body = @request.body.not_nil!.gets_to_end
case json = JSON.parse(body).raw
when Hash
json.each do |key, value|
@json[key] = value.raw
end
when Array
@json["_json"] = json
end
end
private def parse_part(part : IO?)
HTTP::Params.parse(part ? part.gets_to_end : "")
end
private def parse_part(part : String?)
HTTP::Params.parse part.to_s
end
end
end