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.
@@ -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(client_id, client_secret, username, api_key, base_url, api_version, timeout)
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
- def document
20
- @_document ||= Veryfi::Api::Document.new(request)
21
- end
22
-
23
- def line_item
24
- @_line_item ||= Veryfi::Api::LineItem.new(request)
25
- end
26
-
27
- def tag
28
- @_tag ||= Veryfi::Api::Tag.new(request)
29
- end
30
-
31
- def document_tag
32
- @_document_tag ||= Veryfi::Api::DocumentTag.new(request)
33
- end
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
- def self.from_response(status, response)
8
- if response.empty?
9
- VeryfiError.new(format("%<code>d", code: status))
10
- else
11
- VeryfiError.new(format("%<code>d, %<message>s", code: status, message: response["error"]), response)
12
- end
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
- @message = if response.empty?
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
@@ -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, :base_url, :api_version, :timeout
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": timestamp,
88
- "X-Veryfi-Request-Signature": 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": "Ruby Veryfi-Ruby/#{Veryfi::VERSION}",
95
- Accept: "application/json",
96
- "Content-Type": "application/json",
97
- "Client-Id": client_id,
98
- Authorization: "apikey #{username}:#{api_key}"
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 {} if response.body.empty?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Veryfi
4
- VERSION = "3.0.0"
4
+ VERSION = "4.0.0"
5
5
  end