lib/roda/plugins/json_parser.rb
# frozen-string-literal: true require 'json' class Roda module RodaPlugins # The json_parser plugin parses request bodies in JSON format # if the request's content type specifies json. This is mostly # designed for use with JSON API sites. # # This only parses the request body as JSON if the Content-Type # header for the request includes "json". # # The parsed JSON body will be available in +r.POST+, just as a # parsed HTML form body would be. It will also be available in # +r.params+ (which merges +r.GET+ with +r.POST+). module JsonParser DEFAULT_ERROR_HANDLER = proc{|r| r.halt [400, {}, []]} # Handle options for the json_parser plugin: # :error_handler :: A proc to call if an exception is raised when # parsing a JSON request body. The proc is called # with the request object, and should probably call # halt on the request or raise an exception. # :parser :: The parser to use for parsing incoming json. Should be # an object that responds to +call(str)+ and returns the # parsed data. The default is to call JSON.parse. # :include_request :: If true, the parser will be called with the request # object as the second argument, so the parser needs # to respond to +call(str, request)+. # :wrap :: Whether to wrap uploaded JSON data in a hash with a "_json" # key. Without this, calls to +r.params+ will fail if a non-Hash # (such as an array) is uploaded in JSON format. A value of # :always will wrap all values, and a value of :unless_hash will # only wrap values that are not already hashes. def self.configure(app, opts=OPTS) app.opts[:json_parser_error_handler] = opts[:error_handler] || app.opts[:json_parser_error_handler] || DEFAULT_ERROR_HANDLER app.opts[:json_parser_parser] = opts[:parser] || app.opts[:json_parser_parser] || app.opts[:json_parser] || JSON.method(:parse) app.opts[:json_parser_include_request] = opts[:include_request] if opts.has_key?(:include_request) case opts[:wrap] when :unless_hash, :always app.opts[:json_parser_wrap] = opts[:wrap] when nil # Nothing else raise RodaError, "unsupported option value for json_parser plugin :wrap option: #{opts[:wrap].inspect} (should be :unless_hash or :always)" end end module RequestMethods # If the Content-Type header in the request includes "json", # parse the request body as JSON. Ignore an empty request body. def POST env = @env if post_params = (env["roda.json_params"] || env["rack.request.form_hash"]) post_params elsif (input = env["rack.input"]) && content_type =~ /json/ str = _read_json_input(input) return super if str.empty? begin json_params = parse_json(str) rescue roda_class.opts[:json_parser_error_handler].call(self) end wrap = roda_class.opts[:json_parser_wrap] if wrap == :always || (wrap == :unless_hash && !json_params.is_a?(Hash)) json_params = {"_json"=>json_params} end env["roda.json_params"] = json_params env["rack.request.form_input"] = input json_params else super end end private def parse_json(str) args = [str] args << self if roda_class.opts[:json_parser_include_request] roda_class.opts[:json_parser_parser].call(*args) end # Rack 3 dropped requirement that input be rewindable if Rack.release >= '3' def _read_json_input(input) input.read end else def _read_json_input(input) input.rewind str = input.read input.rewind str end end end end register_plugin(:json_parser, JsonParser) end end