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.
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabscanner
4
+ # Raised when configuration is invalid or incomplete
5
+ class ConfigurationError < Error; end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabscanner
4
+ # Raised when API server errors occur (500+ status)
5
+ class ServerError < Error; end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabscanner
4
+ # Raised when API authentication fails (401 status)
5
+ class UnauthorizedError < Error; end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabscanner
4
+ # Raised when API request validation fails (422 status)
5
+ class ValidationError < Error; end
6
+ 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