veryfi 3.0.0 → 4.0.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 +4 -4
- data/.github/workflows/release.yml +1 -1
- data/.github/workflows/test.yml +1 -1
- data/.gitignore +4 -1
- data/.rubocop.yml +5 -1
- data/.ruby-version +1 -1
- data/.yardopts +10 -0
- data/Gemfile.lock +55 -56
- data/README.md +530 -2
- data/Rakefile +21 -0
- data/lib/veryfi/api/any_document.rb +123 -0
- data/lib/veryfi/api/bank_statement.rb +114 -0
- data/lib/veryfi/api/bank_statement_split.rb +66 -0
- data/lib/veryfi/api/business_card.rb +84 -0
- data/lib/veryfi/api/check.rb +127 -0
- data/lib/veryfi/api/classify.rb +53 -0
- data/lib/veryfi/api/document.rb +117 -0
- data/lib/veryfi/api/document_tag.rb +43 -0
- data/lib/veryfi/api/file_payload.rb +23 -0
- data/lib/veryfi/api/line_item.rb +55 -0
- data/lib/veryfi/api/pdf_split.rb +75 -0
- data/lib/veryfi/api/tag.rb +14 -0
- data/lib/veryfi/api/tag_operations.rb +63 -0
- data/lib/veryfi/api/tax_line.rb +71 -0
- data/lib/veryfi/api/w2.rb +90 -0
- data/lib/veryfi/api/w2_split.rb +68 -0
- data/lib/veryfi/api/w8.rb +90 -0
- data/lib/veryfi/api/w9.rb +92 -0
- data/lib/veryfi/client.rb +61 -17
- data/lib/veryfi/configuration.rb +28 -0
- data/lib/veryfi/error.rb +98 -12
- data/lib/veryfi/request.rb +22 -11
- data/lib/veryfi/resource.rb +102 -0
- data/lib/veryfi/version.rb +1 -1
- data/lib/veryfi.rb +64 -0
- data/veryfi.gemspec +24 -2
- metadata +40 -8
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Veryfi
|
|
4
|
+
module Api
|
|
5
|
+
# W-2 splitting endpoints (`/partner/w2s-set/`).
|
|
6
|
+
#
|
|
7
|
+
# Use these when you have a single file containing multiple W-2 forms.
|
|
8
|
+
# Veryfi will split it and process each W-2 separately; you receive a
|
|
9
|
+
# collection that references the individual {W2} ids it produced.
|
|
10
|
+
#
|
|
11
|
+
# @see https://docs.veryfi.com/api/split-and-process-a-w-2/
|
|
12
|
+
class W2Split
|
|
13
|
+
include FilePayload
|
|
14
|
+
|
|
15
|
+
ENDPOINT = "/partner/w2s-set/"
|
|
16
|
+
|
|
17
|
+
attr_reader :request
|
|
18
|
+
|
|
19
|
+
def initialize(request)
|
|
20
|
+
@request = request
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# List previously processed W-2 sets.
|
|
24
|
+
#
|
|
25
|
+
# @param params [Hash] optional query-string parameters
|
|
26
|
+
# @return [Veryfi::Resource]
|
|
27
|
+
def all(params = {})
|
|
28
|
+
request.get(ENDPOINT, params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Fetch a single W-2 set by id.
|
|
32
|
+
#
|
|
33
|
+
# @param id [Integer]
|
|
34
|
+
# @param params [Hash] optional query-string parameters
|
|
35
|
+
# @return [Veryfi::Resource]
|
|
36
|
+
def get(id, params = {})
|
|
37
|
+
request.get("#{ENDPOINT}#{id}/", params)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Upload a multi-W-2 file and split-and-process it.
|
|
41
|
+
#
|
|
42
|
+
# @param raw_params [Hash]
|
|
43
|
+
# @option raw_params [String] :file_path **required.** Local path.
|
|
44
|
+
# @option raw_params [String] :file_name (basename of `:file_path`)
|
|
45
|
+
# @return [Veryfi::Resource]
|
|
46
|
+
def process(raw_params)
|
|
47
|
+
params = raw_params.transform_keys(&:to_sym)
|
|
48
|
+
file_path = params.delete(:file_path)
|
|
49
|
+
file_name = params.delete(:file_name)
|
|
50
|
+
|
|
51
|
+
payload = file_payload(file_path, file_name).merge(params)
|
|
52
|
+
|
|
53
|
+
request.post(ENDPOINT, payload)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# URL variant of {#process}.
|
|
57
|
+
#
|
|
58
|
+
# @param raw_params [Hash]
|
|
59
|
+
# @option raw_params [String] :file_url single URL
|
|
60
|
+
# @option raw_params [Array<String>] :file_urls list of URLs (alternative)
|
|
61
|
+
# @option raw_params [Integer] :max_pages_to_process (`nil` = all pages)
|
|
62
|
+
# @return [Veryfi::Resource]
|
|
63
|
+
def process_url(raw_params)
|
|
64
|
+
request.post(ENDPOINT, raw_params.transform_keys(&:to_sym))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Veryfi
|
|
4
|
+
module Api
|
|
5
|
+
# W-8 BEN-E endpoints (`/partner/w-8ben-e/`).
|
|
6
|
+
#
|
|
7
|
+
# @see https://docs.veryfi.com/api/w-8ben-e/
|
|
8
|
+
class W8
|
|
9
|
+
include FilePayload
|
|
10
|
+
include TagOperations
|
|
11
|
+
|
|
12
|
+
ENDPOINT = "/partner/w-8ben-e/"
|
|
13
|
+
|
|
14
|
+
attr_reader :request
|
|
15
|
+
|
|
16
|
+
def initialize(request)
|
|
17
|
+
@request = request
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# List previously processed W-8 BEN-E documents.
|
|
21
|
+
#
|
|
22
|
+
# @param params [Hash] optional query-string parameters
|
|
23
|
+
# @option params [String] :created_date__gt "YYYY-MM-DD HH:MM:SS" — strictly after
|
|
24
|
+
# @option params [String] :created_date__gte after or equal
|
|
25
|
+
# @option params [String] :created_date__lt strictly before
|
|
26
|
+
# @option params [String] :created_date__lte before or equal
|
|
27
|
+
# @option params [Integer] :page (1)
|
|
28
|
+
# @option params [Integer] :page_size (50)
|
|
29
|
+
# @return [Veryfi::Resource] `{ "documents" => [...] }`
|
|
30
|
+
def all(params = {})
|
|
31
|
+
request.get(ENDPOINT, params)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Fetch a single W-8 by id.
|
|
35
|
+
#
|
|
36
|
+
# @param id [Integer]
|
|
37
|
+
# @param params [Hash] optional query-string parameters
|
|
38
|
+
# @return [Veryfi::Resource]
|
|
39
|
+
def get(id, params = {})
|
|
40
|
+
request.get("#{ENDPOINT}#{id}/", params)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Upload a W-8 file and extract its fields.
|
|
44
|
+
#
|
|
45
|
+
# @param raw_params [Hash]
|
|
46
|
+
# @option raw_params [String] :file_path **required.** Local path.
|
|
47
|
+
# @option raw_params [String] :file_name (basename of `:file_path`)
|
|
48
|
+
# @return [Veryfi::Resource]
|
|
49
|
+
def process(raw_params)
|
|
50
|
+
params = raw_params.transform_keys(&:to_sym)
|
|
51
|
+
file_path = params.delete(:file_path)
|
|
52
|
+
file_name = params.delete(:file_name)
|
|
53
|
+
|
|
54
|
+
payload = file_payload(file_path, file_name).merge(params)
|
|
55
|
+
|
|
56
|
+
request.post(ENDPOINT, payload)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# URL variant of {#process}.
|
|
60
|
+
#
|
|
61
|
+
# @param raw_params [Hash]
|
|
62
|
+
# @option raw_params [String] :file_url **required.**
|
|
63
|
+
# @option raw_params [String] :file_name (basename of `:file_url`)
|
|
64
|
+
# @return [Veryfi::Resource]
|
|
65
|
+
def process_url(raw_params)
|
|
66
|
+
params = raw_params.transform_keys(&:to_sym)
|
|
67
|
+
params[:file_name] ||= File.basename(params[:file_url]) if params[:file_url]
|
|
68
|
+
|
|
69
|
+
request.post(ENDPOINT, params)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Update writable fields on a processed W-8.
|
|
73
|
+
#
|
|
74
|
+
# @param id [Integer]
|
|
75
|
+
# @param params [Hash]
|
|
76
|
+
# @return [Veryfi::Resource]
|
|
77
|
+
def update(id, params)
|
|
78
|
+
request.put("#{ENDPOINT}#{id}/", params)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Delete a W-8.
|
|
82
|
+
#
|
|
83
|
+
# @param id [Integer]
|
|
84
|
+
# @return [Veryfi::Resource]
|
|
85
|
+
def delete(id)
|
|
86
|
+
request.delete("#{ENDPOINT}#{id}/")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Veryfi
|
|
4
|
+
module Api
|
|
5
|
+
# W-9 endpoints (`/partner/w9s/`).
|
|
6
|
+
#
|
|
7
|
+
# @see https://docs.veryfi.com/api/w9s/
|
|
8
|
+
class W9
|
|
9
|
+
include FilePayload
|
|
10
|
+
include TagOperations
|
|
11
|
+
|
|
12
|
+
ENDPOINT = "/partner/w9s/"
|
|
13
|
+
|
|
14
|
+
attr_reader :request
|
|
15
|
+
|
|
16
|
+
def initialize(request)
|
|
17
|
+
@request = request
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# List previously processed W-9 documents.
|
|
21
|
+
#
|
|
22
|
+
# @param params [Hash] optional query-string parameters
|
|
23
|
+
# @option params [String] :created_date__gt "YYYY-MM-DD HH:MM:SS" — strictly after
|
|
24
|
+
# @option params [String] :created_date__gte after or equal
|
|
25
|
+
# @option params [String] :created_date__lt strictly before
|
|
26
|
+
# @option params [String] :created_date__lte before or equal
|
|
27
|
+
# @option params [Integer] :page (1)
|
|
28
|
+
# @option params [Integer] :page_size (50)
|
|
29
|
+
# @return [Veryfi::Resource] `{ "documents" => [...] }`
|
|
30
|
+
def all(params = {})
|
|
31
|
+
request.get(ENDPOINT, params)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Fetch a single W-9 by id.
|
|
35
|
+
#
|
|
36
|
+
# @param id [Integer]
|
|
37
|
+
# @param params [Hash] optional query-string parameters
|
|
38
|
+
# @option params [Boolean] :bounding_boxes (`false`) Include bounding-box info.
|
|
39
|
+
# @option params [Boolean] :confidence_details (`false`) Include per-field confidence scores.
|
|
40
|
+
# @return [Veryfi::Resource]
|
|
41
|
+
def get(id, params = {})
|
|
42
|
+
request.get("#{ENDPOINT}#{id}/", params)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Upload a W-9 file and extract its fields.
|
|
46
|
+
#
|
|
47
|
+
# @param raw_params [Hash]
|
|
48
|
+
# @option raw_params [String] :file_path **required.** Local path.
|
|
49
|
+
# @option raw_params [String] :file_name (basename of `:file_path`)
|
|
50
|
+
# @return [Veryfi::Resource]
|
|
51
|
+
def process(raw_params)
|
|
52
|
+
params = raw_params.transform_keys(&:to_sym)
|
|
53
|
+
file_path = params.delete(:file_path)
|
|
54
|
+
file_name = params.delete(:file_name)
|
|
55
|
+
|
|
56
|
+
payload = file_payload(file_path, file_name).merge(params)
|
|
57
|
+
|
|
58
|
+
request.post(ENDPOINT, payload)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# URL variant of {#process}.
|
|
62
|
+
#
|
|
63
|
+
# @param raw_params [Hash]
|
|
64
|
+
# @option raw_params [String] :file_url **required.**
|
|
65
|
+
# @option raw_params [String] :file_name (basename of `:file_url`)
|
|
66
|
+
# @return [Veryfi::Resource]
|
|
67
|
+
def process_url(raw_params)
|
|
68
|
+
params = raw_params.transform_keys(&:to_sym)
|
|
69
|
+
params[:file_name] ||= File.basename(params[:file_url]) if params[:file_url]
|
|
70
|
+
|
|
71
|
+
request.post(ENDPOINT, params)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Update writable fields on a processed W-9.
|
|
75
|
+
#
|
|
76
|
+
# @param id [Integer]
|
|
77
|
+
# @param params [Hash]
|
|
78
|
+
# @return [Veryfi::Resource]
|
|
79
|
+
def update(id, params)
|
|
80
|
+
request.put("#{ENDPOINT}#{id}/", params)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Delete a W-9.
|
|
84
|
+
#
|
|
85
|
+
# @param id [Integer]
|
|
86
|
+
# @return [Veryfi::Resource]
|
|
87
|
+
def delete(id)
|
|
88
|
+
request.delete("#{ENDPOINT}#{id}/")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/veryfi/client.rb
CHANGED
|
@@ -1,7 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Veryfi
|
|
4
|
+
# The user-facing entry point.
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# client = Veryfi::Client.new(
|
|
8
|
+
# client_id: ENV["VERYFI_CLIENT_ID"],
|
|
9
|
+
# client_secret: ENV["VERYFI_CLIENT_SECRET"],
|
|
10
|
+
# username: ENV["VERYFI_USERNAME"],
|
|
11
|
+
# api_key: ENV["VERYFI_API_KEY"]
|
|
12
|
+
# )
|
|
13
|
+
# client.document.process(file_path: "./receipt.jpg")
|
|
14
|
+
#
|
|
15
|
+
# @example Custom Faraday configuration (persistent connections + retries)
|
|
16
|
+
# client = Veryfi::Client.new(
|
|
17
|
+
# client_id: "…",
|
|
18
|
+
# client_secret: "…",
|
|
19
|
+
# username: "…",
|
|
20
|
+
# api_key: "…",
|
|
21
|
+
# faraday: ->(conn) {
|
|
22
|
+
# conn.request :retry, max: 3, interval: 0.5, backoff_factor: 2,
|
|
23
|
+
# retry_statuses: [429, 502, 503, 504]
|
|
24
|
+
# conn.response :logger, Rails.logger if defined?(Rails)
|
|
25
|
+
# conn.adapter :net_http_persistent
|
|
26
|
+
# }
|
|
27
|
+
# )
|
|
4
28
|
class Client
|
|
29
|
+
# DSL: declare an API namespace as a lazily-memoized reader.
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# api_namespace :document, Veryfi::Api::Document
|
|
33
|
+
#
|
|
34
|
+
# @param name [Symbol]
|
|
35
|
+
# @param klass [Class] API class accepting a `Veryfi::Request` in its constructor
|
|
36
|
+
# @return [void]
|
|
37
|
+
def self.api_namespace(name, klass)
|
|
38
|
+
ivar = :"@_#{name}"
|
|
39
|
+
define_method(name) do
|
|
40
|
+
instance_variable_get(ivar) || instance_variable_set(ivar, klass.new(request))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
5
44
|
attr_reader :request
|
|
6
45
|
|
|
7
46
|
def initialize(
|
|
@@ -11,26 +50,31 @@ module Veryfi
|
|
|
11
50
|
api_key:,
|
|
12
51
|
base_url: "https://api.veryfi.com/api/",
|
|
13
52
|
api_version: "v8",
|
|
14
|
-
timeout: 20
|
|
53
|
+
timeout: 20,
|
|
54
|
+
faraday: nil
|
|
15
55
|
)
|
|
16
|
-
@request = Veryfi::Request.new(
|
|
56
|
+
@request = Veryfi::Request.new(
|
|
57
|
+
client_id, client_secret, username, api_key,
|
|
58
|
+
base_url, api_version, timeout, faraday
|
|
59
|
+
)
|
|
17
60
|
end
|
|
18
61
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
api_namespace :document, Veryfi::Api::Document
|
|
63
|
+
api_namespace :line_item, Veryfi::Api::LineItem
|
|
64
|
+
api_namespace :tax_line, Veryfi::Api::TaxLine
|
|
65
|
+
api_namespace :tag, Veryfi::Api::Tag
|
|
66
|
+
api_namespace :document_tag, Veryfi::Api::DocumentTag
|
|
67
|
+
api_namespace :any_document, Veryfi::Api::AnyDocument
|
|
68
|
+
api_namespace :bank_statement, Veryfi::Api::BankStatement
|
|
69
|
+
api_namespace :bank_statement_split, Veryfi::Api::BankStatementSplit
|
|
70
|
+
api_namespace :business_card, Veryfi::Api::BusinessCard
|
|
71
|
+
api_namespace :check, Veryfi::Api::Check
|
|
72
|
+
api_namespace :classify, Veryfi::Api::Classify
|
|
73
|
+
api_namespace :pdf_split, Veryfi::Api::PdfSplit
|
|
74
|
+
api_namespace :w2, Veryfi::Api::W2
|
|
75
|
+
api_namespace :w2_split, Veryfi::Api::W2Split
|
|
76
|
+
api_namespace :w8, Veryfi::Api::W8
|
|
77
|
+
api_namespace :w9, Veryfi::Api::W9
|
|
34
78
|
|
|
35
79
|
def api_url
|
|
36
80
|
request.api_url
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Veryfi
|
|
4
|
+
# Process-wide settings used by {Veryfi.client} to build the shared
|
|
5
|
+
# singleton client. Mirrors the keyword arguments of
|
|
6
|
+
# {Veryfi::Client#initialize}; defaults match the client's defaults.
|
|
7
|
+
class Configuration
|
|
8
|
+
ATTRS = %i[
|
|
9
|
+
client_id client_secret username api_key
|
|
10
|
+
base_url api_version timeout faraday
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
attr_accessor(*ATTRS)
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@base_url = "https://api.veryfi.com/api/"
|
|
17
|
+
@api_version = "v8"
|
|
18
|
+
@timeout = 20
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Hash] the configuration as a keyword-arg-ready Hash. Keys
|
|
22
|
+
# with `nil` values are still included; {Veryfi.client} calls
|
|
23
|
+
# `.compact` before passing it to {Veryfi::Client#initialize}.
|
|
24
|
+
def to_h
|
|
25
|
+
ATTRS.to_h { |attr| [attr, public_send(attr)] }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/veryfi/error.rb
CHANGED
|
@@ -3,30 +3,116 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module Veryfi
|
|
6
|
+
# Namespace + factory for every error raised by this SDK.
|
|
7
|
+
#
|
|
8
|
+
# All errors inherit from {VeryfiError}, so callers that only need to
|
|
9
|
+
# know "something went wrong with Veryfi" can keep using:
|
|
10
|
+
#
|
|
11
|
+
# begin
|
|
12
|
+
# client.document.process(file_path: path)
|
|
13
|
+
# rescue Veryfi::Error::VeryfiError => e
|
|
14
|
+
# # …
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Callers that want to react differently per HTTP status can rescue a
|
|
18
|
+
# more specific subclass:
|
|
19
|
+
#
|
|
20
|
+
# begin
|
|
21
|
+
# client.document.process(file_path: path)
|
|
22
|
+
# rescue Veryfi::Error::Unauthorized then refresh_credentials!
|
|
23
|
+
# rescue Veryfi::Error::TooManyRequests then back_off
|
|
24
|
+
# rescue Veryfi::Error::ServerError then schedule_retry
|
|
25
|
+
# rescue Veryfi::Error::VeryfiError then log_and_raise
|
|
26
|
+
# end
|
|
6
27
|
class Error
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
end
|
|
14
|
-
|
|
28
|
+
# Base class for every Veryfi SDK error.
|
|
29
|
+
#
|
|
30
|
+
# `#message` returns the pretty-printed JSON error payload when one is
|
|
31
|
+
# available, otherwise the formatted `"<status>"` / `"<status>, <error>"`
|
|
32
|
+
# string. The `#status` and `#response` accessors give callers
|
|
33
|
+
# programmatic access to the same information.
|
|
15
34
|
class VeryfiError < StandardError
|
|
16
|
-
attr_reader :message
|
|
35
|
+
attr_reader :message, :status, :response
|
|
17
36
|
|
|
18
|
-
def initialize(message = "An error occurred", response = {})
|
|
19
|
-
@
|
|
37
|
+
def initialize(message = "An error occurred", response = {}, status = nil)
|
|
38
|
+
@status = status
|
|
39
|
+
@response = response
|
|
40
|
+
@message = if response.nil? || response.empty?
|
|
20
41
|
message
|
|
21
42
|
else
|
|
22
43
|
JSON.pretty_generate(response)
|
|
23
44
|
end
|
|
24
|
-
super(message)
|
|
45
|
+
super(@message)
|
|
25
46
|
end
|
|
26
47
|
|
|
27
48
|
def to_s
|
|
28
49
|
message
|
|
29
50
|
end
|
|
30
51
|
end
|
|
52
|
+
|
|
53
|
+
# 400 — request was malformed or failed server-side validation.
|
|
54
|
+
class BadRequest < VeryfiError; end
|
|
55
|
+
# 401 — credentials are missing, invalid, or expired.
|
|
56
|
+
class Unauthorized < VeryfiError; end
|
|
57
|
+
# 403 — credentials are valid but lack permission for this resource.
|
|
58
|
+
class AccessLimitReached < VeryfiError; end
|
|
59
|
+
# 404 — the resource id does not exist (or has been deleted).
|
|
60
|
+
class NotFound < VeryfiError; end
|
|
61
|
+
# 408 — the request timed out before Veryfi could respond.
|
|
62
|
+
class RequestTimeout < VeryfiError; end
|
|
63
|
+
# 409 — request conflicts with current resource state.
|
|
64
|
+
class Conflict < VeryfiError; end
|
|
65
|
+
# 415 — uploaded file type is not supported by the endpoint.
|
|
66
|
+
class UnsupportedMediaType < VeryfiError; end
|
|
67
|
+
# 429 — you've hit a rate limit. Back off and retry.
|
|
68
|
+
class TooManyRequests < VeryfiError; end
|
|
69
|
+
# Catch-all for any other 4xx the server returns.
|
|
70
|
+
class ClientError < VeryfiError; end
|
|
71
|
+
# 5xx — Veryfi reported an internal error. Retrying with backoff is usually safe.
|
|
72
|
+
class ServerError < VeryfiError; end
|
|
73
|
+
|
|
74
|
+
STATUS_MAP = {
|
|
75
|
+
400 => BadRequest,
|
|
76
|
+
401 => Unauthorized,
|
|
77
|
+
403 => AccessLimitReached,
|
|
78
|
+
404 => NotFound,
|
|
79
|
+
408 => RequestTimeout,
|
|
80
|
+
409 => Conflict,
|
|
81
|
+
415 => UnsupportedMediaType,
|
|
82
|
+
429 => TooManyRequests
|
|
83
|
+
}.freeze
|
|
84
|
+
private_constant :STATUS_MAP
|
|
85
|
+
|
|
86
|
+
# Build the right error subclass for the given HTTP status + response
|
|
87
|
+
# body. Always returns an instance of {VeryfiError} or one of its
|
|
88
|
+
# subclasses; never raises.
|
|
89
|
+
#
|
|
90
|
+
# @param status [Integer] HTTP status code
|
|
91
|
+
# @param response [Hash, Veryfi::Resource, nil] parsed JSON body
|
|
92
|
+
# @return [VeryfiError]
|
|
93
|
+
def self.from_response(status, response)
|
|
94
|
+
klass = error_class_for(status)
|
|
95
|
+
message = format_message(status, response)
|
|
96
|
+
|
|
97
|
+
klass.new(message, response, status)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.error_class_for(status)
|
|
101
|
+
return STATUS_MAP[status] if STATUS_MAP.key?(status)
|
|
102
|
+
return ServerError if status.between?(500, 599)
|
|
103
|
+
return ClientError if status.between?(400, 499)
|
|
104
|
+
|
|
105
|
+
VeryfiError
|
|
106
|
+
end
|
|
107
|
+
private_class_method :error_class_for
|
|
108
|
+
|
|
109
|
+
def self.format_message(status, response)
|
|
110
|
+
if response.nil? || response.empty?
|
|
111
|
+
format("%<code>d", code: status)
|
|
112
|
+
else
|
|
113
|
+
format("%<code>d, %<message>s", code: status, message: response["error"])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
private_class_method :format_message
|
|
31
117
|
end
|
|
32
118
|
end
|
data/lib/veryfi/request.rb
CHANGED
|
@@ -5,8 +5,16 @@ require "faraday"
|
|
|
5
5
|
require "json"
|
|
6
6
|
|
|
7
7
|
module Veryfi
|
|
8
|
+
# Low-level HTTP layer used by every API class. You typically don't need
|
|
9
|
+
# to interact with this directly — go through {Veryfi::Client} instead.
|
|
10
|
+
#
|
|
11
|
+
# Custom Faraday configuration (adapter, retries, logging, persistent
|
|
12
|
+
# connections, …) can be supplied via the `faraday:` block when
|
|
13
|
+
# constructing the client. The block receives the `Faraday::Connection`
|
|
14
|
+
# before it's frozen, so you can attach any middleware you want.
|
|
8
15
|
class Request
|
|
9
|
-
attr_reader :client_id, :client_secret, :username, :api_key,
|
|
16
|
+
attr_reader :client_id, :client_secret, :username, :api_key,
|
|
17
|
+
:base_url, :api_version, :timeout, :faraday_block
|
|
10
18
|
|
|
11
19
|
VERBS_WITH_BODIES = %i[post put].freeze
|
|
12
20
|
|
|
@@ -17,7 +25,8 @@ module Veryfi
|
|
|
17
25
|
api_key,
|
|
18
26
|
base_url,
|
|
19
27
|
api_version,
|
|
20
|
-
timeout
|
|
28
|
+
timeout,
|
|
29
|
+
faraday_block = nil
|
|
21
30
|
)
|
|
22
31
|
@client_id = client_id
|
|
23
32
|
@client_secret = client_secret
|
|
@@ -26,6 +35,7 @@ module Veryfi
|
|
|
26
35
|
@base_url = base_url
|
|
27
36
|
@api_version = api_version
|
|
28
37
|
@timeout = timeout
|
|
38
|
+
@faraday_block = faraday_block
|
|
29
39
|
end
|
|
30
40
|
|
|
31
41
|
def get(path, params = {})
|
|
@@ -68,6 +78,7 @@ module Veryfi
|
|
|
68
78
|
def conn
|
|
69
79
|
@_conn ||= Faraday.new do |conn|
|
|
70
80
|
conn.options.timeout = timeout
|
|
81
|
+
faraday_block&.call(conn)
|
|
71
82
|
end
|
|
72
83
|
end
|
|
73
84
|
|
|
@@ -84,18 +95,18 @@ module Veryfi
|
|
|
84
95
|
signature = generate_signature(params, timestamp)
|
|
85
96
|
|
|
86
97
|
default_headers.merge(
|
|
87
|
-
"X-Veryfi-Request-Timestamp"
|
|
88
|
-
"X-Veryfi-Request-Signature"
|
|
98
|
+
"X-Veryfi-Request-Timestamp" => timestamp,
|
|
99
|
+
"X-Veryfi-Request-Signature" => signature
|
|
89
100
|
)
|
|
90
101
|
end
|
|
91
102
|
|
|
92
103
|
def default_headers
|
|
93
104
|
{
|
|
94
|
-
"User-Agent"
|
|
95
|
-
Accept
|
|
96
|
-
"Content-Type"
|
|
97
|
-
"Client-Id"
|
|
98
|
-
Authorization
|
|
105
|
+
"User-Agent" => "Ruby Veryfi-Ruby/#{Veryfi::VERSION}",
|
|
106
|
+
"Accept" => "application/json",
|
|
107
|
+
"Content-Type" => "application/json",
|
|
108
|
+
"Client-Id" => client_id,
|
|
109
|
+
"Authorization" => "apikey #{username}:#{api_key}"
|
|
99
110
|
}
|
|
100
111
|
end
|
|
101
112
|
|
|
@@ -104,9 +115,9 @@ module Veryfi
|
|
|
104
115
|
end
|
|
105
116
|
|
|
106
117
|
def process_response(response)
|
|
107
|
-
return
|
|
118
|
+
return Veryfi::Resource.new if response.body.empty?
|
|
108
119
|
|
|
109
|
-
JSON.parse(response.body)
|
|
120
|
+
Veryfi::Resource.wrap(JSON.parse(response.body))
|
|
110
121
|
end
|
|
111
122
|
end
|
|
112
123
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Veryfi
|
|
4
|
+
# A lightweight, dependency-free wrapper around an API response payload.
|
|
5
|
+
#
|
|
6
|
+
# `Resource` inherits from `Hash`, so anything that already treats the
|
|
7
|
+
# response as a hash keeps working unchanged:
|
|
8
|
+
#
|
|
9
|
+
# response["id"] # => 44691518
|
|
10
|
+
# response.dig("vendor", "name")
|
|
11
|
+
# response.is_a?(Hash) # => true
|
|
12
|
+
# JSON.pretty_generate(response)
|
|
13
|
+
#
|
|
14
|
+
# In addition, every key is also accessible as a method, recursively:
|
|
15
|
+
#
|
|
16
|
+
# response.id # => 44691518
|
|
17
|
+
# response.vendor.name # => "East Repair"
|
|
18
|
+
# response.line_items.first.description
|
|
19
|
+
# response.is_duplicate? # => truthiness of self["is_duplicate"]
|
|
20
|
+
#
|
|
21
|
+
# Nested hashes are wrapped into Resources, and arrays of hashes become
|
|
22
|
+
# arrays of Resources. Other values (strings, numbers, booleans, nil)
|
|
23
|
+
# pass through untouched. Both string (`"id"`) and symbol (`:id`) keys
|
|
24
|
+
# work transparently.
|
|
25
|
+
class Resource < ::Hash
|
|
26
|
+
# Wrap any value coming back from the API. Hashes become Resources,
|
|
27
|
+
# arrays are mapped recursively, and everything else passes through.
|
|
28
|
+
#
|
|
29
|
+
# @param value [Object] raw value from `JSON.parse`
|
|
30
|
+
# @return [Object] wrapped value
|
|
31
|
+
def self.wrap(value)
|
|
32
|
+
return value if value.is_a?(Resource)
|
|
33
|
+
|
|
34
|
+
case value
|
|
35
|
+
when ::Hash then new(value)
|
|
36
|
+
when ::Array then value.map { |v| wrap(v) }
|
|
37
|
+
else value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(hash = {})
|
|
42
|
+
super()
|
|
43
|
+
hash.each_pair { |key, value| self[key.to_s] = Resource.wrap(value) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def [](key)
|
|
47
|
+
super(key.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# rubocop:disable Style/ArgumentsForwarding -- keep explicit forwarding for clarity and Ruby 3.0 portability
|
|
51
|
+
def fetch(key, *args, &block)
|
|
52
|
+
super(key.to_s, *args, &block)
|
|
53
|
+
end
|
|
54
|
+
# rubocop:enable Style/ArgumentsForwarding
|
|
55
|
+
|
|
56
|
+
def key?(key)
|
|
57
|
+
super(key.to_s)
|
|
58
|
+
end
|
|
59
|
+
alias has_key? key?
|
|
60
|
+
alias include? key?
|
|
61
|
+
alias member? key?
|
|
62
|
+
|
|
63
|
+
# Returns a plain (unwrapped) `Hash` representation, recursively.
|
|
64
|
+
# Useful when you need to hand the data off to something that explicitly
|
|
65
|
+
# expects a plain Hash (e.g. some serializers).
|
|
66
|
+
#
|
|
67
|
+
# @return [Hash]
|
|
68
|
+
def to_h
|
|
69
|
+
each_with_object({}) do |(key, value), memo|
|
|
70
|
+
memo[key] = unwrap(value)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
alias to_hash to_h
|
|
74
|
+
|
|
75
|
+
def respond_to_missing?(name, include_private = false)
|
|
76
|
+
string_name = name.to_s.chomp("?")
|
|
77
|
+
key?(string_name) || super
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def method_missing(name, *args, &block)
|
|
81
|
+
string_name = name.to_s
|
|
82
|
+
bare_name = string_name.chomp("?")
|
|
83
|
+
|
|
84
|
+
if args.empty? && block.nil? && key?(bare_name)
|
|
85
|
+
value = self[bare_name]
|
|
86
|
+
string_name.end_with?("?") ? !value.nil? && value != false : value
|
|
87
|
+
else
|
|
88
|
+
super
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def unwrap(value)
|
|
95
|
+
case value
|
|
96
|
+
when Resource then value.to_h
|
|
97
|
+
when ::Array then value.map { |v| v.is_a?(Resource) ? v.to_h : v }
|
|
98
|
+
else value
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/veryfi/version.rb
CHANGED