require 'cgi'
module Underpass
class ErrorParser
PATTERNS = {
timeout: /Query timed out.*?at line (\d+) after (\d+) seconds/i,
memory: /Query run out of memory.*?(\d+)\s*MB/i,
syntax: /parse error:?\s*(.+)/i,
runtime: /runtime error:?\s*(.+)/i
}.freeze
def self.parse(response_body, status_code)
return rate_limit_result if status_code == 429
text = (response_body)
parse_error_text(text, status_code)
end
def self.(body)
return '' if body.nil? || body.empty?
paragraphs = (body)
from_paragraphs = pick_paragraph(paragraphs)
return CGI.unescapeHTML(from_paragraphs) if from_paragraphs
from_strong = (body)
return CGI.unescapeHTML(from_strong) unless from_strong.empty?
CGI.unescapeHTML(body.gsub(/<[^>]+>/, ' ').gsub(/\s+/, ' ').strip)
end
private_class_method :extract_error_text
def self.(body)
body.scan(%r{<p[^>]*>(.*?)</p>}im)
.map { |m| m[0].gsub(/<[^>]+>/, '').strip }
.reject(&:empty?)
end
private_class_method :extract_paragraphs
def self.pick_paragraph(paragraphs)
return if paragraphs.empty?
matched = paragraphs.find { |p| PATTERNS.each_value.any? { |rx| p.match?(rx) } }
matched || paragraphs.last
end
private_class_method :pick_paragraph
def self.(body)
match = body.match(%r{<strong[^>]*>(.*?)</strong>}im)
return '' unless match
match[1].gsub(/<[^>]+>/, '').strip
end
private_class_method :extract_strong_text
def self.parse_error_text(text, status_code)
parse_timeout(text) ||
parse_memory(text) ||
parse_syntax(text) ||
parse_runtime(text) ||
unknown_result(text, status_code)
end
private_class_method :parse_error_text
def self.parse_timeout(text)
return unless (match = text.match(PATTERNS[:timeout]))
{ code: 'timeout', message: text, details: { line: match[1].to_i, timeout_seconds: match[2].to_i } }
end
private_class_method :parse_timeout
def self.parse_memory(text)
return unless (match = text.match(PATTERNS[:memory]))
{ code: 'memory', message: text, details: { memory_mb: match[1].to_i } }
end
private_class_method :parse_memory
def self.parse_syntax(text)
return unless text.match(PATTERNS[:syntax])
{ code: 'syntax', message: text.strip, details: (text) }
end
private_class_method :parse_syntax
def self.parse_runtime(text)
return unless (match = text.match(PATTERNS[:runtime]))
{ code: 'runtime', message: match[1].strip, details: {} }
end
private_class_method :parse_runtime
def self.(message)
if (match = message.match(/line\s+(\d+)/i))
{ line: match[1].to_i }
else
{}
end
end
private_class_method :extract_syntax_details
def self.rate_limit_result
{
code: 'rate_limit',
message: 'Rate limited by the Overpass API',
details: {}
}
end
private_class_method :rate_limit_result
def self.unknown_result(text, status_code)
message = text.empty? ? "HTTP #{status_code} error" : text
{
code: 'unknown',
message: message,
details: {}
}
end
private_class_method :unknown_result
end
end