<commit_before>require "active_support/concern"
require "active_support/inflector"
require "active_support/core_ext/object/blank"

require "iknow_params/serializer"

# IknowParams::Parser provides a mix-in for ActiveRecord controllers to parse input parameters.
module IknowParams::Parser
  extend ActiveSupport::Concern

  class ParseError < Exception; end

  PARAM_REQUIRED = Object.new
  BLANK = Object.new

  # Parse the specified parameter, optionally deserializing with the provided
  # IKnowParams::Serializer. If the parameter is missing and no default is
  # provided, raises a ParseError.
  #
  # If `BLANK` is provided as a default, return a placeholder object that can be
  # later stripped out with `remove_blanks`
  #
  # If `dump` is true, use the serializer to re-serialize any successfully
  # parsed argument back to a canonical string. This can be useful to validate
  # and normalize the input to another service without parsing it. A serializer
  # must be passed to use this option.
  def parse_param(param, with: nil, default: PARAM_REQUIRED, dump: false)
    serializer =
      case with
      when String, Symbol
        IknowParams::Serializer.for!(with)
      else
        with
      end

    val = params[param]

    parse =
        if val.nil?
          raise ParseError.new("Required parameter '#{param}' missing") if default == PARAM_REQUIRED
          default
        elsif serializer.present?
          begin
            serializer.load(val)
          rescue Exception => ex
            raise ParseError.new("Invalid parameter '#{param}': '#{val.inspect}' - #{ex.message}")
          end
        else
          val
        end

    if dump && parse != BLANK
      parse = serializer.dump(parse)
    end

    parse
  end

  # Parse an array-typed param using the provided serializer for each member element.
  def parse_array_param(param, with: nil, default: PARAM_REQUIRED, dump: false)
    serializer =
      case with
      when String, Symbol
        IknowParams::Serializer.for!(with)
      else
        with
      end

    vals = params[param]

    parses =
      if vals.nil?
        raise ParseError.new("Required parameter '#{param}' missing") if default == PARAM_REQUIRED
        default
      elsif !vals.is_a?(Array)
        raise ParseError.new("Invalid type for parameter '#{param}': '#{vals.class.name}'")
      elsif serializer.present?
        vals.map do |val|
          begin
            serializer.load(val)
          rescue Exception => ex
            raise ParseError.new("Invalid member in parameter '#{param}': '#{val.inspect}' - #{ex.message}")
          end
        end
      else
        vals
      end

    if dump && parses != BLANK
      parses.map! { |v| serializer.dump(v) }
    end

    parses
  end

  # Convenience method to make it simpler to build a hash structure with
  # optional members from parsed data. This method recursively traverses the
  # provided structure and removes any instances of the sentinel value
  # Parser::BLANK.
  def remove_blanks(arg)
    case arg
    when Hash
      arg.each do |k, v|
        if v == BLANK
          arg.delete(k)
        else
          remove_blanks(v)
        end<commit_msg>Add metadata to parse errors<commit_after>require "active_support/concern"
require "active_support/inflector"
require "active_support/core_ext/object/blank"

require "iknow_params/serializer"

# IknowParams::Parser provides a mix-in for ActiveRecord controllers to parse input parameters.
module IknowParams::Parser
  extend ActiveSupport::Concern

  class ParseError < Exception
    attr_accessor :param, :value

    def initialize(message, param, value)
      super(message)
      self.param = param
      self.value = value
    end
  end

  PARAM_REQUIRED = Object.new
  BLANK = Object.new

  # Parse the specified parameter, optionally deserializing with the provided
  # IKnowParams::Serializer. If the parameter is missing and no default is
  # provided, raises a ParseError.
  #
  # If `BLANK` is provided as a default, return a placeholder object that can be
  # later stripped out with `remove_blanks`
  #
  # If `dump` is true, use the serializer to re-serialize any successfully
  # parsed argument back to a canonical string. This can be useful to validate
  # and normalize the input to another service without parsing it. A serializer
  # must be passed to use this option.
  def parse_param(param, with: nil, default: PARAM_REQUIRED, dump: false)
    serializer =
      case with
      when String, Symbol
        IknowParams::Serializer.for!(with)
      else
        with
      end

    val = params[param]

    parse =
        if val.nil?
          raise ParseError.new("Required parameter '#{param}' missing", param, nil) if default == PARAM_REQUIRED
          default
        elsif serializer.present?
          begin
            serializer.load(val)
          rescue Exception => ex
            raise ParseError.new("Invalid parameter '#{param}': '#{val.inspect}' - #{ex.message}", param, val)
          end
        else
          val
        end

    if dump && parse != BLANK
      parse = serializer.dump(parse)
    end

    parse
  end

  # Parse an array-typed param using the provided serializer for each member element.
  def parse_array_param(param, with: nil, default: PARAM_REQUIRED, dump: false)
    serializer =
      case with
      when String, Symbol
        IknowParams::Serializer.for!(with)
      else
        with
      end

    vals = params[param]

    parses =
      if vals.nil?
        raise ParseError.new("Required parameter '#{param}' missing", param, nil) if default == PARAM_REQUIRED
        default
      elsif !vals.is_a?(Array)
        raise ParseError.new("Invalid type for parameter '#{param}': '#{vals.class.name}'", param, vals)
      elsif serializer.present?
        vals.map do |val|
          begin
            serializer.load(val)
          rescue Exception => ex
            raise ParseError.new("Invalid member in array parameter '#{param}': '#{val.inspect}' - #{ex.message}", param, val)
          end
        end
      else
        vals
      end

    if dump && parses != BLANK
      parses.map! { |v| serializer.dump(v) }
    end

    parses
  end

  # Convenience method to make it simpler to build a hash structure with
  # optional members from parsed data. This method recursively traverses the
  # provided structure and removes any instances of the sentinel value
  # Parser::BLANK.
  def remove_blanks(arg)
    case arg
    when Hash
      arg.each do |k, v|
        if v == BLANK
          arg.delete(k)
        else
          remove_blanks(v)
        end
{'fraction_non_alphanumeric': 0.08124433705829055, 'fraction_numerical': 0.0, 'mean_word_length': 2.5191285866099893, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '784186', 'n_tokens_mistral': 1880, 'n_tokens_neox': 1840, 'n_words': 647}