module Jekyll::Algolia::ErrorHandler

def self.error_hash(message)

by our error matchers
Returns a hash of all parts of the message, to be more easily consumed

message - The raw message as returned by the API

Public: Parses an Algolia error message into a hash of its content
def self.error_hash(message)
  message = message.delete("\n")
  # Ex: Cannot PUT to https://appid.algolia.net/1/indexes/index_name/settings:
  # {"message":"Invalid Application-ID or API key","status":403} (403)
  regex = VerEx.new do
    find 'Cannot '
    capture('verb') { word }
    find ' to '
    capture('scheme') { word }
    find '://'
    capture('application_id') { word }
    anything_but '/'
    find '/'
    capture('api_version') { digit }
    find '/'
    capture('api_section') { word }
    find '/'
    capture('index_name') do
      anything_but('/')
    end
    find '/'
    capture do
      capture('api_action') { word }
      maybe '?'
      capture('query_parameters') do
        anything_but(':')
      end
    end
    find ': '
    capture('json') do
      find '{'
      anything_but('}')
      find '}'
    end
    find ' ('
    capture('http_error') { word }
    find ')'
  end
  matches = regex.match(message)
  return false unless matches
  # Convert matches to a hash
  hash = {}
  matches.names.each do |name|
    hash[name] = matches[name]
  end
  hash['api_version'] = hash['api_version'].to_i
  hash['http_error'] = hash['http_error'].to_i
  # Merging the JSON key directly in the answer
  hash = hash.merge(JSON.parse(hash['json']))
  hash.delete('json')
  # Merging the query parameters in the answer
  CGI.parse(hash['query_parameters']).each do |key, values|
    hash[key] = values[0]
  end
  hash.delete('query_parameters')
  hash
end

def self.identify(error, context = {})

further identifying the issue.
that matches. Returns false if no match, or a hash of :name and :details
It will parse in order all potential known issues until it finds one

code intercepting the user
context - A hash of additional information that can be passed from the
error - The caught error

Public: Will identify the error and return its internal name
def self.identify(error, context = {})
  known_errors = %w[
    unknown_application_id
    invalid_credentials
    record_too_big
    unknown_settings
    invalid_index_name
  ]
  # Checking the errors against our known list
  known_errors.each do |potential_error|
    error_check = send("#{potential_error}?", error, context)
    next if error_check == false
    return {
      name: potential_error,
      details: error_check
    }
  end
  false
end

def self.invalid_credentials?(error, _context = {})

Application ID and API key submitted don't match any credentials known

_context - Not used

Public: Check if the credentials are working
def self.invalid_credentials?(error, _context = {})
  details = error_hash(error.message)
  if details['message'] != 'Invalid Application-ID or API key'
    return false
  end
  {
    'application_id' => details['application_id']
  }
end

def self.invalid_index_name?(error, _context = {})

Some characters are forbidden in index names

Public: Check if the index name is invalid
def self.invalid_index_name?(error, _context = {})
  details = error_hash(error.message)
  message = details['message']
  return false if message !~ /^indexName is not valid.*/
  {
    'index_name' => Configurator.index_name
  }
end

def self.readable_largest_record_keys(record)

finding which record is causing trouble
This will be used on the `record_too_big` error, to guide users in

record - The record hash to analyze

the record
Public: Returns a string explaining which attributes are the largest in
def self.readable_largest_record_keys(record)
  keys = Hash[record.map { |key, value| [key, value.to_s.length] }]
  largest_keys = keys.sort_by { |_, value| value }.reverse[0..2]
  output = []
  largest_keys.each do |key, size|
    size = Filesize.from("#{size} B").to_s('Kb')
    output << "#{key} (#{size})"
  end
  output.join(', ')
end

def self.record_too_big?(error, context = {})

informations about it so the user can debug it.
means that one of the records is too big, so we'll try to give
Records cannot weight more that 10Kb. If we're getting this error it

context[:records] - list of records sent in the batch

Public: Check if the sent records are not too big
def self.record_too_big?(error, context = {})
  details = error_hash(error.message)
  message = details['message']
  return false if message !~ /^Record .* is too big .*/
  # Getting the record size
  size, = /.*size=(.*) bytes.*/.match(message).captures
  size = Filesize.from("#{size} B").to_s('Kb')
  object_id = details['objectID']
  # Getting record details
  record = Utils.find_by_key(context[:records], :objectID, object_id)
  probable_wrong_keys = readable_largest_record_keys(record)
  # Writing the full record to disk for inspection
  record_log_path = Logger.write_to_file(
    "jekyll-algolia-record-too-big-#{object_id}.log",
    JSON.pretty_generate(record)
  )
  {
    'object_id' => object_id,
    'object_title' => record[:title],
    'object_url' => record[:url],
    'probable_wrong_keys' => probable_wrong_keys,
    'record_log_path' => record_log_path,
    'nodes_to_index' => Configurator.algolia('nodes_to_index'),
    'size' => size,
    'size_limit' => '10 Kb'
  }
end

def self.stop(error, context = {})

happened to the display
context - A hash of values that will be passed from where the error
error - The caught error

a human-readable error message
Public: Stop the execution of the plugin and display if possible
def self.stop(error, context = {})
  Logger.verbose("E:[jekyll-algolia] Raw error: #{error}")
  identified_error = identify(error, context)
  if identified_error == false
    Logger.log('E:[jekyll-algolia] Error:')
    Logger.log("E:#{error}")
  else
    Logger.known_message(
      identified_error[:name],
      identified_error[:details]
    )
  end
  exit 1
end

def self.unknown_application_id?(error, _context = {})

and does not follow our error spec
is invalid. As we cannot actually contact the server, the error is raw
If the call to the cluster fails, chances are that the application ID

_context - Not used

Public: Check if the application id is available
def self.unknown_application_id?(error, _context = {})
  message = error.message
  return false if message !~ /^Cannot reach any host/
  matches = /.*\((.*)\.algolia.net.*/.match(message)
  # The API will browse on APP_ID-dsn, but push/delete on APP_ID only
  # We need to catch both potential errors
  app_id = matches[1].gsub(/-dsn$/, '')
  { 'application_id' => app_id }
end

def self.unknown_settings?(error, context = {})

issue.
not available. We'll tell the user which one so they can fix their
The API will block any call that tries to update a setting value that is

context[:settings] - The settings passed to update the index

Public: Check if one of the index settings is invalid
def self.unknown_settings?(error, context = {})
  details = error_hash(error.message)
  message = details['message']
  return false if message !~ /^Invalid object attributes.*/
  # Getting the unknown setting name
  regex = /^Invalid object attributes: (.*) near line.*/
  setting_name, = regex.match(message).captures
  setting_value = context[:settings][setting_name]
  {
    'setting_name' => setting_name,
    'setting_value' => setting_value
  }
end