tabscanner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/README.md +582 -0
- data/Rakefile +8 -0
- data/docs/architecture.md +124 -0
- data/docs/prd.md +124 -0
- data/docs/stories/1.1.story.md +229 -0
- data/docs/stories/1.2.story.md +255 -0
- data/docs/stories/1.3.story.md +246 -0
- data/docs/stories/1.4.story.md +152 -0
- data/docs/stories/1.5.story.md +149 -0
- data/docs/stories/1.6.story.md +166 -0
- data/docs/stories/2.1.story.md +216 -0
- data/examples/README.md +85 -0
- data/examples/batch_process.rb +56 -0
- data/examples/check_credits.rb +75 -0
- data/examples/process_receipt.rb +12 -0
- data/examples/quick_test.rb +80 -0
- data/lib/tabscanner/client.rb +50 -0
- data/lib/tabscanner/config.rb +101 -0
- data/lib/tabscanner/credits.rb +63 -0
- data/lib/tabscanner/errors/base_error.rb +55 -0
- data/lib/tabscanner/errors/configuration_error.rb +6 -0
- data/lib/tabscanner/errors/server_error.rb +6 -0
- data/lib/tabscanner/errors/unauthorized_error.rb +6 -0
- data/lib/tabscanner/errors/validation_error.rb +6 -0
- data/lib/tabscanner/http_client.rb +108 -0
- data/lib/tabscanner/request.rb +227 -0
- data/lib/tabscanner/result.rb +192 -0
- data/lib/tabscanner/version.rb +5 -0
- data/lib/tabscanner.rb +43 -0
- data/sig/tabscanner.rbs +4 -0
- metadata +149 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Example: Check remaining API credits
|
5
|
+
#
|
6
|
+
# This example demonstrates how to check your remaining API credits
|
7
|
+
# for the Tabscanner service. This is useful for monitoring usage
|
8
|
+
# and staying within the 200 free plan limit.
|
9
|
+
|
10
|
+
require_relative '../lib/tabscanner'
|
11
|
+
require 'logger'
|
12
|
+
|
13
|
+
# Configure the Tabscanner gem
|
14
|
+
Tabscanner.configure do |config|
|
15
|
+
# Set your API key from environment variable or directly
|
16
|
+
config.api_key = ENV['TABSCANNER_API_KEY'] || 'your-api-key-here'
|
17
|
+
|
18
|
+
# Optional: Enable debug mode for detailed logging
|
19
|
+
config.debug = ENV['TABSCANNER_DEBUG'] == 'true'
|
20
|
+
|
21
|
+
# Optional: Set custom base URL if needed
|
22
|
+
# config.base_url = 'https://custom.api.example.com'
|
23
|
+
end
|
24
|
+
|
25
|
+
begin
|
26
|
+
puts "Checking remaining API credits..."
|
27
|
+
|
28
|
+
# Check credits
|
29
|
+
credits = Tabscanner.get_credits
|
30
|
+
|
31
|
+
puts "✅ Remaining credits: #{credits}"
|
32
|
+
|
33
|
+
# Provide usage guidance based on credit count
|
34
|
+
if credits == 0
|
35
|
+
puts "⚠️ Warning: You have no credits remaining!"
|
36
|
+
puts " Please upgrade your plan or wait for credits to reset."
|
37
|
+
elsif credits < 10
|
38
|
+
puts "⚠️ Warning: Low credit balance (#{credits} remaining)"
|
39
|
+
puts " Consider upgrading your plan or monitoring usage carefully."
|
40
|
+
elsif credits < 50
|
41
|
+
puts "ℹ️ Credit balance is getting low (#{credits} remaining)"
|
42
|
+
else
|
43
|
+
puts "✨ You have plenty of credits available!"
|
44
|
+
end
|
45
|
+
|
46
|
+
rescue Tabscanner::ConfigurationError => e
|
47
|
+
puts "❌ Configuration error: #{e.message}"
|
48
|
+
puts " Please make sure TABSCANNER_API_KEY is set in your environment."
|
49
|
+
puts " Example: export TABSCANNER_API_KEY='your-actual-api-key'"
|
50
|
+
exit 1
|
51
|
+
|
52
|
+
rescue Tabscanner::UnauthorizedError => e
|
53
|
+
puts "❌ Authentication failed: #{e.message}"
|
54
|
+
puts " Please check that your API key is valid and active."
|
55
|
+
puts " You can get your API key from the Tabscanner dashboard."
|
56
|
+
exit 1
|
57
|
+
|
58
|
+
rescue Tabscanner::ServerError => e
|
59
|
+
puts "❌ Server error: #{e.message}"
|
60
|
+
puts " The Tabscanner service is experiencing issues. Please try again later."
|
61
|
+
exit 1
|
62
|
+
|
63
|
+
rescue Tabscanner::Error => e
|
64
|
+
puts "❌ Unexpected error: #{e.message}"
|
65
|
+
if Tabscanner.config.debug?
|
66
|
+
puts "\nDebug Information:"
|
67
|
+
puts e.raw_response.inspect if e.respond_to?(:raw_response)
|
68
|
+
end
|
69
|
+
exit 1
|
70
|
+
|
71
|
+
rescue StandardError => e
|
72
|
+
puts "❌ Unexpected system error: #{e.message}"
|
73
|
+
puts " #{e.class}: #{e.backtrace.first}"
|
74
|
+
exit 1
|
75
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Simple receipt processing script - under 10 lines of code
|
3
|
+
require_relative '../lib/tabscanner'
|
4
|
+
|
5
|
+
Tabscanner.configure { |c| c.api_key = ENV['TABSCANNER_API_KEY'] }
|
6
|
+
|
7
|
+
token = Tabscanner.submit_receipt(ARGV[0])
|
8
|
+
result = Tabscanner.get_result(token)
|
9
|
+
|
10
|
+
puts "Merchant: #{result['merchant']}"
|
11
|
+
puts "Total: $#{result['total']}"
|
12
|
+
puts "Items: #{result['items']&.count || 0}"
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Quick test script to verify gem functionality
|
3
|
+
require_relative '../lib/tabscanner'
|
4
|
+
|
5
|
+
# Test configuration
|
6
|
+
begin
|
7
|
+
Tabscanner.configure do |config|
|
8
|
+
config.api_key = ENV['TABSCANNER_API_KEY'] || 'test_key'
|
9
|
+
config.debug = true
|
10
|
+
end
|
11
|
+
|
12
|
+
puts "✅ Configuration successful"
|
13
|
+
puts " API Key: #{Tabscanner.config.api_key ? '[SET]' : '[NOT SET]'}"
|
14
|
+
puts " Region: #{Tabscanner.config.region}"
|
15
|
+
puts " Debug: #{Tabscanner.config.debug?}"
|
16
|
+
rescue => e
|
17
|
+
puts "❌ Configuration failed: #{e.message}"
|
18
|
+
exit 1
|
19
|
+
end
|
20
|
+
|
21
|
+
# Test validation
|
22
|
+
begin
|
23
|
+
Tabscanner.config.validate!
|
24
|
+
puts "✅ Configuration validation passed"
|
25
|
+
rescue Tabscanner::ConfigurationError => e
|
26
|
+
puts "❌ Configuration validation failed: #{e.message}"
|
27
|
+
puts " Please set TABSCANNER_API_KEY environment variable"
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
|
31
|
+
# Test credits functionality
|
32
|
+
puts "\n" + "="*50
|
33
|
+
puts "Testing Credits Functionality"
|
34
|
+
puts "="*50
|
35
|
+
|
36
|
+
begin
|
37
|
+
credits = Tabscanner.get_credits
|
38
|
+
puts "✅ Credits check successful"
|
39
|
+
puts " 🏦 Your remaining credits: #{credits}"
|
40
|
+
|
41
|
+
# Provide usage guidance
|
42
|
+
case credits
|
43
|
+
when 0
|
44
|
+
puts " ⚠️ WARNING: No credits remaining!"
|
45
|
+
puts " 📝 Action needed: Upgrade your plan or wait for reset"
|
46
|
+
when 1..9
|
47
|
+
puts " ⚠️ WARNING: Very low credit balance!"
|
48
|
+
puts " 📝 Recommendation: Consider upgrading soon"
|
49
|
+
when 10..49
|
50
|
+
puts " ℹ️ Info: Credits getting low"
|
51
|
+
puts " 📝 Suggestion: Monitor usage carefully"
|
52
|
+
when 50..99
|
53
|
+
puts " ✨ Good: Moderate credit balance"
|
54
|
+
else
|
55
|
+
puts " 💰 Excellent: Plenty of credits available!"
|
56
|
+
end
|
57
|
+
|
58
|
+
rescue Tabscanner::UnauthorizedError => e
|
59
|
+
puts "❌ Credits check failed - Authentication error"
|
60
|
+
puts " Error: #{e.message}"
|
61
|
+
puts " 🔑 Please verify your API key is correct"
|
62
|
+
rescue Tabscanner::ServerError => e
|
63
|
+
puts "❌ Credits check failed - Server error"
|
64
|
+
puts " Error: #{e.message}"
|
65
|
+
puts " 🔧 The service may be temporarily unavailable"
|
66
|
+
rescue Tabscanner::Error => e
|
67
|
+
puts "❌ Credits check failed - API error"
|
68
|
+
puts " Error: #{e.message}"
|
69
|
+
rescue => e
|
70
|
+
puts "❌ Credits check failed - Unexpected error"
|
71
|
+
puts " Error: #{e.class}: #{e.message}"
|
72
|
+
end
|
73
|
+
|
74
|
+
puts "\n🎉 Gem is properly configured and ready to use!"
|
75
|
+
puts "\nTo process a receipt:"
|
76
|
+
puts " ruby examples/process_receipt.rb path/to/receipt.jpg"
|
77
|
+
puts "\nTo batch process receipts:"
|
78
|
+
puts " ruby examples/batch_process.rb receipts_directory"
|
79
|
+
puts "\nTo check credits only:"
|
80
|
+
puts " ruby examples/check_credits.rb"
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tabscanner
|
4
|
+
# Central public interface for the Tabscanner gem
|
5
|
+
#
|
6
|
+
# This module provides the main public API methods for interacting
|
7
|
+
# with the Tabscanner service, delegating to appropriate internal classes.
|
8
|
+
module Client
|
9
|
+
# Submit a receipt image for OCR processing
|
10
|
+
#
|
11
|
+
# @param file_path_or_io [String, IO] Local file path or IO stream containing image data
|
12
|
+
# @return [String] Token for later result retrieval
|
13
|
+
# @raise [ConfigurationError] when configuration is invalid
|
14
|
+
# @raise [UnauthorizedError] when API key is invalid (401)
|
15
|
+
# @raise [ValidationError] when request validation fails (422)
|
16
|
+
# @raise [ServerError] when server errors occur (500+)
|
17
|
+
# @raise [Error] for other API errors
|
18
|
+
#
|
19
|
+
# @example Submit a file by path
|
20
|
+
# token = Tabscanner.submit_receipt('/path/to/receipt.jpg')
|
21
|
+
#
|
22
|
+
# @example Submit a file via IO stream
|
23
|
+
# File.open('/path/to/receipt.jpg', 'rb') do |file|
|
24
|
+
# token = Tabscanner.submit_receipt(file)
|
25
|
+
# end
|
26
|
+
def self.submit_receipt(file_path_or_io)
|
27
|
+
Request.submit_receipt(file_path_or_io)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Poll for OCR processing results using a token
|
31
|
+
#
|
32
|
+
# @param token [String] Token from submit_receipt call
|
33
|
+
# @param timeout [Integer] Maximum time to wait in seconds (default: 15)
|
34
|
+
# @return [Hash] Parsed receipt data when processing is complete
|
35
|
+
# @raise [ConfigurationError] when configuration is invalid
|
36
|
+
# @raise [UnauthorizedError] when API key is invalid (401)
|
37
|
+
# @raise [ValidationError] when token is invalid (422)
|
38
|
+
# @raise [ServerError] when server errors occur (500+)
|
39
|
+
# @raise [Error] for timeout or other API errors
|
40
|
+
#
|
41
|
+
# @example Poll for results with default timeout
|
42
|
+
# data = Tabscanner.get_result('token123')
|
43
|
+
#
|
44
|
+
# @example Poll for results with custom timeout
|
45
|
+
# data = Tabscanner.get_result('token123', timeout: 30)
|
46
|
+
def self.get_result(token, timeout: 15)
|
47
|
+
Result.get_result(token, timeout: timeout)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Tabscanner
|
6
|
+
# Configuration class implementing singleton pattern
|
7
|
+
#
|
8
|
+
# This class manages global configuration for the Tabscanner gem.
|
9
|
+
# It implements the singleton pattern to ensure consistent configuration
|
10
|
+
# across the entire application.
|
11
|
+
#
|
12
|
+
# @example Basic configuration
|
13
|
+
# Tabscanner.configure do |config|
|
14
|
+
# config.api_key = 'your-key'
|
15
|
+
# config.region = 'us'
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# @example Debug configuration
|
19
|
+
# Tabscanner.configure do |config|
|
20
|
+
# config.api_key = 'your-key'
|
21
|
+
# config.debug = true
|
22
|
+
# config.logger = Logger.new(STDOUT)
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @example Access current configuration
|
26
|
+
# Tabscanner.config.api_key
|
27
|
+
class Config
|
28
|
+
# @!attribute [rw] api_key
|
29
|
+
# @return [String, nil] The API key for Tabscanner service
|
30
|
+
# @!attribute [rw] region
|
31
|
+
# @return [String] The region for API calls (default: 'us')
|
32
|
+
# @!attribute [rw] base_url
|
33
|
+
# @return [String, nil] Override base URL for API calls
|
34
|
+
# @!attribute [rw] debug
|
35
|
+
# @return [Boolean] Enable debug logging and enhanced error details (default: false)
|
36
|
+
# @!attribute [rw] logger
|
37
|
+
# @return [Logger] Logger instance for debug output (default: Logger.new(STDOUT))
|
38
|
+
attr_accessor :api_key, :region, :base_url, :debug, :logger
|
39
|
+
|
40
|
+
# Initialize configuration with default values from environment variables
|
41
|
+
def initialize
|
42
|
+
@api_key = ENV['TABSCANNER_API_KEY']
|
43
|
+
@region = ENV['TABSCANNER_REGION'] || 'us'
|
44
|
+
@base_url = ENV['TABSCANNER_BASE_URL']
|
45
|
+
@debug = ENV['TABSCANNER_DEBUG'] == 'true' || false
|
46
|
+
@logger = nil # Will be created lazily in logger method
|
47
|
+
end
|
48
|
+
|
49
|
+
# Thread-safe singleton instance access
|
50
|
+
# @return [Config] the singleton instance
|
51
|
+
def self.instance
|
52
|
+
@instance ||= new
|
53
|
+
end
|
54
|
+
|
55
|
+
# Reset the singleton instance (primarily for testing)
|
56
|
+
# @api private
|
57
|
+
def self.reset!
|
58
|
+
@instance = nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get or create the logger instance
|
62
|
+
# @return [Logger] Logger instance for debug output
|
63
|
+
def logger
|
64
|
+
@logger ||= Logger.new(STDOUT).tap do |log|
|
65
|
+
log.level = debug? ? Logger::DEBUG : Logger::WARN
|
66
|
+
log.formatter = proc do |severity, datetime, progname, msg|
|
67
|
+
"[#{datetime}] #{severity} -- Tabscanner: #{msg}\n"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check if debug mode is enabled
|
73
|
+
# @return [Boolean] true if debug mode is enabled
|
74
|
+
def debug?
|
75
|
+
!!@debug
|
76
|
+
end
|
77
|
+
|
78
|
+
# Validate that required configuration is present
|
79
|
+
# @raise [ConfigurationError] if required configuration is missing
|
80
|
+
def validate!
|
81
|
+
raise Tabscanner::ConfigurationError, "API key is required" if api_key.nil? || api_key.empty?
|
82
|
+
raise Tabscanner::ConfigurationError, "Region cannot be empty" if region.nil? || region.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
private_class_method :new
|
86
|
+
end
|
87
|
+
|
88
|
+
# Configure the gem with a block
|
89
|
+
# @yield [Config] the configuration instance
|
90
|
+
# @return [Config] the configuration instance
|
91
|
+
def self.configure
|
92
|
+
yield(Config.instance) if block_given?
|
93
|
+
Config.instance
|
94
|
+
end
|
95
|
+
|
96
|
+
# Access the current configuration
|
97
|
+
# @return [Config] the singleton configuration instance
|
98
|
+
def self.config
|
99
|
+
Config.instance
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'http_client'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Tabscanner
|
7
|
+
# Handles credit balance retrieval from the Tabscanner API
|
8
|
+
#
|
9
|
+
# This class manages HTTP requests to check the remaining API credits
|
10
|
+
# for the authenticated account.
|
11
|
+
#
|
12
|
+
# @example Check remaining credits
|
13
|
+
# Credits.get_credits
|
14
|
+
class Credits
|
15
|
+
extend HttpClient
|
16
|
+
# Get remaining API credits for the authenticated account
|
17
|
+
#
|
18
|
+
# @return [Integer] Number of remaining credits
|
19
|
+
# @raise [UnauthorizedError] when API key is invalid (401)
|
20
|
+
# @raise [ServerError] when server errors occur (500+)
|
21
|
+
# @raise [Error] for other API errors or JSON parsing issues
|
22
|
+
def self.get_credits
|
23
|
+
config = Tabscanner.config
|
24
|
+
config.validate!
|
25
|
+
|
26
|
+
# Build the connection
|
27
|
+
conn = build_connection(config, additional_headers: { 'Accept' => 'application/json' })
|
28
|
+
|
29
|
+
# Make the GET request to credit endpoint
|
30
|
+
response = conn.get('/api/credit')
|
31
|
+
|
32
|
+
# Debug logging for request/response
|
33
|
+
log_request_response('GET', '/api/credit', response, config) if config.debug?
|
34
|
+
|
35
|
+
handle_response_with_common_errors(response) do |resp|
|
36
|
+
parse_credits_response(resp)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Parse credits response to extract integer credit count
|
43
|
+
# @param response [Faraday::Response] HTTP response
|
44
|
+
# @return [Integer] Credit count
|
45
|
+
# @raise [Error] if response cannot be parsed as integer
|
46
|
+
def self.parse_credits_response(response)
|
47
|
+
begin
|
48
|
+
# API returns a single JSON number
|
49
|
+
credit_count = JSON.parse(response.body)
|
50
|
+
|
51
|
+
# Ensure we got a numeric value
|
52
|
+
unless credit_count.is_a?(Numeric)
|
53
|
+
raise Error, "Invalid credit response format: expected number, got #{credit_count.class}"
|
54
|
+
end
|
55
|
+
|
56
|
+
credit_count.to_i
|
57
|
+
rescue JSON::ParserError
|
58
|
+
raise Error, "Invalid JSON response from API"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tabscanner
|
4
|
+
# Base error class for all Tabscanner-specific errors
|
5
|
+
#
|
6
|
+
# This class provides enhanced error handling capabilities including
|
7
|
+
# raw response data for debugging purposes when debug mode is enabled.
|
8
|
+
#
|
9
|
+
# @example Basic error
|
10
|
+
# raise Tabscanner::Error, "Something went wrong"
|
11
|
+
#
|
12
|
+
# @example Error with raw response for debugging
|
13
|
+
# response = { status: 500, body: '{"error": "Server error"}' }
|
14
|
+
# raise Tabscanner::Error.new("Server error", raw_response: response)
|
15
|
+
class Error < StandardError
|
16
|
+
# @return [Hash, nil] Raw HTTP response data for debugging
|
17
|
+
attr_reader :raw_response
|
18
|
+
|
19
|
+
# Initialize error with message and optional raw response
|
20
|
+
# @param message [String] Error message
|
21
|
+
# @param raw_response [Hash, nil] Raw HTTP response data for debugging
|
22
|
+
def initialize(message = nil, raw_response: nil)
|
23
|
+
@raw_response = raw_response
|
24
|
+
|
25
|
+
# Enhance message with debug info if available and debug mode enabled
|
26
|
+
enhanced_message = build_enhanced_message(message)
|
27
|
+
super(enhanced_message)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Build enhanced error message with debug information
|
33
|
+
# @param base_message [String] Base error message
|
34
|
+
# @return [String] Enhanced message with debug info if enabled
|
35
|
+
def build_enhanced_message(base_message)
|
36
|
+
return base_message unless Tabscanner.config.debug? && @raw_response
|
37
|
+
|
38
|
+
debug_info = []
|
39
|
+
|
40
|
+
if @raw_response.is_a?(Hash)
|
41
|
+
debug_info << "Status: #{@raw_response[:status]}" if @raw_response[:status]
|
42
|
+
debug_info << "Headers: #{@raw_response[:headers]}" if @raw_response[:headers]
|
43
|
+
debug_info << "Body: #{@raw_response[:body]}" if @raw_response[:body]
|
44
|
+
else
|
45
|
+
debug_info << "Raw Response: #{@raw_response.inspect}"
|
46
|
+
end
|
47
|
+
|
48
|
+
if debug_info.any?
|
49
|
+
"#{base_message}\n\nDebug Information:\n#{debug_info.join("\n")}"
|
50
|
+
else
|
51
|
+
base_message
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faraday'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Tabscanner
|
7
|
+
# Shared HTTP client functionality for Tabscanner API requests
|
8
|
+
#
|
9
|
+
# This module provides common HTTP connection building, error handling,
|
10
|
+
# and logging functionality used across Request, Result, and Credits classes.
|
11
|
+
#
|
12
|
+
# @example Include in a class
|
13
|
+
# class MyAPIClass
|
14
|
+
# extend Tabscanner::HttpClient
|
15
|
+
# end
|
16
|
+
module HttpClient
|
17
|
+
# Build Faraday connection with proper configuration
|
18
|
+
# @param config [Config] Configuration instance
|
19
|
+
# @param additional_headers [Hash] Additional headers to include
|
20
|
+
# @return [Faraday::Connection] Configured connection
|
21
|
+
def build_connection(config, additional_headers: {})
|
22
|
+
base_url = config.base_url || "https://api.tabscanner.com"
|
23
|
+
|
24
|
+
Faraday.new(url: base_url) do |f|
|
25
|
+
f.request :url_encoded
|
26
|
+
f.adapter Faraday.default_adapter
|
27
|
+
f.headers['apikey'] = config.api_key
|
28
|
+
f.headers['User-Agent'] = "Tabscanner Ruby Gem #{Tabscanner::VERSION}"
|
29
|
+
|
30
|
+
# Merge any additional headers
|
31
|
+
additional_headers.each { |key, value| f.headers[key] = value }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Build raw response data for error debugging
|
36
|
+
# @param response [Faraday::Response] HTTP response
|
37
|
+
# @return [Hash] Raw response data
|
38
|
+
def build_raw_response_data(response)
|
39
|
+
{
|
40
|
+
status: response.status,
|
41
|
+
headers: response.headers.to_hash,
|
42
|
+
body: response.body
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
# Parse error message from response
|
47
|
+
# @param response [Faraday::Response] HTTP response
|
48
|
+
# @return [String, nil] Error message if available
|
49
|
+
def parse_error_message(response)
|
50
|
+
return nil if response.body.nil? || response.body.empty?
|
51
|
+
|
52
|
+
begin
|
53
|
+
data = JSON.parse(response.body)
|
54
|
+
data['error'] || data['message'] || data['errors']&.first
|
55
|
+
rescue JSON::ParserError
|
56
|
+
# If JSON parsing fails, return raw body if it's short enough
|
57
|
+
response.body.length < 200 ? response.body : nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Handle common HTTP status codes with appropriate errors
|
62
|
+
# @param response [Faraday::Response] HTTP response
|
63
|
+
# @param success_handler [Proc] Block to handle successful responses
|
64
|
+
# @return [Object] Result from success_handler
|
65
|
+
# @raise [UnauthorizedError, ServerError, Error] Based on status code
|
66
|
+
def handle_response_with_common_errors(response, &success_handler)
|
67
|
+
raw_response = build_raw_response_data(response)
|
68
|
+
|
69
|
+
case response.status
|
70
|
+
when 200, 201
|
71
|
+
success_handler.call(response)
|
72
|
+
when 401
|
73
|
+
raise UnauthorizedError.new("Invalid API key or authentication failed", raw_response: raw_response)
|
74
|
+
when 500..599
|
75
|
+
error_message = parse_error_message(response) || "Server error occurred"
|
76
|
+
raise ServerError.new(error_message, raw_response: raw_response)
|
77
|
+
else
|
78
|
+
error_message = parse_error_message(response) || "Request failed with status #{response.status}"
|
79
|
+
raise Error.new(error_message, raw_response: raw_response)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Log request and response details for debugging
|
84
|
+
# @param method [String] HTTP method
|
85
|
+
# @param endpoint [String] API endpoint
|
86
|
+
# @param response [Faraday::Response] HTTP response
|
87
|
+
# @param config [Config] Configuration instance
|
88
|
+
def log_request_response(method, endpoint, response, config)
|
89
|
+
logger = config.logger
|
90
|
+
|
91
|
+
# Log request details
|
92
|
+
logger.debug("HTTP Request: #{method.upcase} #{endpoint}")
|
93
|
+
logger.debug("Request Headers: apikey=[REDACTED], User-Agent=#{response.env.request_headers['User-Agent']}")
|
94
|
+
|
95
|
+
# Log response details
|
96
|
+
logger.debug("HTTP Response: #{response.status}")
|
97
|
+
logger.debug("Response Headers: #{response.headers.to_hash}")
|
98
|
+
|
99
|
+
# Log response body (truncated if too long)
|
100
|
+
body = response.body
|
101
|
+
if body && body.length > 500
|
102
|
+
logger.debug("Response Body: #{body[0..500]}... (truncated)")
|
103
|
+
else
|
104
|
+
logger.debug("Response Body: #{body}")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|