didit-sdk 1.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 +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/lib/didit/authentication/base64_url.rb +34 -0
- data/lib/didit/authentication/cache.rb +201 -0
- data/lib/didit/authentication/http.rb +150 -0
- data/lib/didit/authentication/issuer.rb +115 -0
- data/lib/didit/authentication/jws.rb +231 -0
- data/lib/didit/authentication/key_set.rb +89 -0
- data/lib/didit/authentication/pkce.rb +64 -0
- data/lib/didit/authentication/types.rb +175 -0
- data/lib/didit/authentication.rb +546 -0
- data/lib/didit/client.rb +211 -0
- data/lib/didit/errors.rb +153 -0
- data/lib/didit/file_part.rb +85 -0
- data/lib/didit/internal/config.rb +94 -0
- data/lib/didit/internal/errors.rb +83 -0
- data/lib/didit/internal/log.rb +41 -0
- data/lib/didit/internal/multipart.rb +60 -0
- data/lib/didit/internal/request.rb +322 -0
- data/lib/didit/internal/transport.rb +198 -0
- data/lib/didit/internal/util.rb +76 -0
- data/lib/didit/internal.rb +15 -0
- data/lib/didit/model.rb +199 -0
- data/lib/didit/page.rb +130 -0
- data/lib/didit/raw_response.rb +57 -0
- data/lib/didit/resource.rb +15 -0
- data/lib/didit/resources/account.rb +309 -0
- data/lib/didit/resources/address_validation.rb +73 -0
- data/lib/didit/resources/age_estimation.rb +110 -0
- data/lib/didit/resources/aml.rb +166 -0
- data/lib/didit/resources/aml_monitoring.rb +30 -0
- data/lib/didit/resources/analytics.rb +87 -0
- data/lib/didit/resources/bank.rb +65 -0
- data/lib/didit/resources/billing.rb +61 -0
- data/lib/didit/resources/biometric_templates.rb +234 -0
- data/lib/didit/resources/blocklist.rb +130 -0
- data/lib/didit/resources/business_sessions.rb +29 -0
- data/lib/didit/resources/businesses.rb +471 -0
- data/lib/didit/resources/cases.rb +2116 -0
- data/lib/didit/resources/customization.rb +153 -0
- data/lib/didit/resources/database_validation.rb +327 -0
- data/lib/didit/resources/document_ai.rb +145 -0
- data/lib/didit/resources/email.rb +254 -0
- data/lib/didit/resources/face_match.rb +103 -0
- data/lib/didit/resources/face_search.rb +103 -0
- data/lib/didit/resources/fraud_checks.rb +130 -0
- data/lib/didit/resources/id_verification.rb +163 -0
- data/lib/didit/resources/id_wallets.rb +1468 -0
- data/lib/didit/resources/investigator.rb +419 -0
- data/lib/didit/resources/ip.rb +68 -0
- data/lib/didit/resources/kyb.rb +256 -0
- data/lib/didit/resources/lists.rb +332 -0
- data/lib/didit/resources/monitoring_events.rb +466 -0
- data/lib/didit/resources/networks.rb +427 -0
- data/lib/didit/resources/passive_liveness.rb +103 -0
- data/lib/didit/resources/phone.rb +271 -0
- data/lib/didit/resources/pricing.rb +40 -0
- data/lib/didit/resources/proof_of_address.rb +167 -0
- data/lib/didit/resources/questionnaires.rb +170 -0
- data/lib/didit/resources/regulatory_reports.rb +314 -0
- data/lib/didit/resources/report_templates.rb +146 -0
- data/lib/didit/resources/sessions.rb +2820 -0
- data/lib/didit/resources/transactions.rb +981 -0
- data/lib/didit/resources/travel_rule.rb +1202 -0
- data/lib/didit/resources/users.rb +521 -0
- data/lib/didit/resources/wallet_screening.rb +190 -0
- data/lib/didit/resources/webhooks.rb +396 -0
- data/lib/didit/resources/workflows.rb +214 -0
- data/lib/didit/resources.rb +263 -0
- data/lib/didit/types/account.rb +11 -0
- data/lib/didit/types/account_components.rb +105 -0
- data/lib/didit/types/address_validation.rb +8 -0
- data/lib/didit/types/age_estimation.rb +89 -0
- data/lib/didit/types/aml.rb +193 -0
- data/lib/didit/types/aml_monitoring.rb +73 -0
- data/lib/didit/types/analytics.rb +62 -0
- data/lib/didit/types/bank.rb +8 -0
- data/lib/didit/types/billing.rb +18 -0
- data/lib/didit/types/biometric_templates.rb +292 -0
- data/lib/didit/types/blocklist.rb +31 -0
- data/lib/didit/types/businesses.rb +45 -0
- data/lib/didit/types/cases.rb +368 -0
- data/lib/didit/types/components.rb +12206 -0
- data/lib/didit/types/customization.rb +8 -0
- data/lib/didit/types/database_validation.rb +177 -0
- data/lib/didit/types/document_ai.rb +135 -0
- data/lib/didit/types/email.rb +116 -0
- data/lib/didit/types/face_match.rb +93 -0
- data/lib/didit/types/face_search.rb +129 -0
- data/lib/didit/types/fraud_checks.rb +8 -0
- data/lib/didit/types/id_verification.rb +231 -0
- data/lib/didit/types/id_wallets.rb +8 -0
- data/lib/didit/types/investigator.rb +221 -0
- data/lib/didit/types/ip.rb +8 -0
- data/lib/didit/types/kyb.rb +1391 -0
- data/lib/didit/types/lists.rb +111 -0
- data/lib/didit/types/monitoring_events.rb +235 -0
- data/lib/didit/types/networks.rb +120 -0
- data/lib/didit/types/passive_liveness.rb +95 -0
- data/lib/didit/types/phone.rb +69 -0
- data/lib/didit/types/proof_of_address.rb +166 -0
- data/lib/didit/types/questionnaires.rb +29 -0
- data/lib/didit/types/regulatory_reports.rb +100 -0
- data/lib/didit/types/report_templates.rb +41 -0
- data/lib/didit/types/sessions.rb +1441 -0
- data/lib/didit/types/transactions.rb +245 -0
- data/lib/didit/types/travel_rule.rb +168 -0
- data/lib/didit/types/users.rb +180 -0
- data/lib/didit/types/wallet_screening.rb +49 -0
- data/lib/didit/types/webhooks.rb +355 -0
- data/lib/didit/types/workflows.rb +32 -0
- data/lib/didit/types.rb +1681 -0
- data/lib/didit/version.rb +5 -0
- data/lib/didit/webhooks/canonical_json.rb +187 -0
- data/lib/didit/webhooks/client_methods.rb +23 -0
- data/lib/didit/webhooks/events.rb +221 -0
- data/lib/didit/webhooks/headers.rb +83 -0
- data/lib/didit/webhooks/verifier.rb +260 -0
- data/lib/didit/webhooks.rb +99 -0
- data/lib/didit-sdk.rb +4 -0
- data/lib/didit.rb +31 -0
- data/sig/didit/authentication/base64_url.rbs +10 -0
- data/sig/didit/authentication/cache.rbs +60 -0
- data/sig/didit/authentication/http.rbs +37 -0
- data/sig/didit/authentication/issuer.rbs +17 -0
- data/sig/didit/authentication/jws.rbs +41 -0
- data/sig/didit/authentication/jwt.rbs +14 -0
- data/sig/didit/authentication/key_set.rbs +25 -0
- data/sig/didit/authentication/pkce.rbs +20 -0
- data/sig/didit/authentication/types.rbs +114 -0
- data/sig/didit/authentication.rbs +81 -0
- data/sig/didit/client.rbs +61 -0
- data/sig/didit/errors.rbs +80 -0
- data/sig/didit/file_part.rbs +19 -0
- data/sig/didit/internal.rbs +199 -0
- data/sig/didit/model.rbs +42 -0
- data/sig/didit/page.rbs +34 -0
- data/sig/didit/raw_response.rbs +20 -0
- data/sig/didit/resource.rbs +8 -0
- data/sig/didit/resources/account.rbs +23 -0
- data/sig/didit/resources/address_validation.rbs +9 -0
- data/sig/didit/resources/age_estimation.rbs +9 -0
- data/sig/didit/resources/aml.rbs +10 -0
- data/sig/didit/resources/aml_monitoring.rbs +9 -0
- data/sig/didit/resources/analytics.rbs +9 -0
- data/sig/didit/resources/bank.rbs +9 -0
- data/sig/didit/resources/billing.rbs +10 -0
- data/sig/didit/resources/biometric_templates.rbs +13 -0
- data/sig/didit/resources/blocklist.rbs +11 -0
- data/sig/didit/resources/business_sessions.rbs +9 -0
- data/sig/didit/resources/businesses.rbs +22 -0
- data/sig/didit/resources/cases.rbs +103 -0
- data/sig/didit/resources/customization.rbs +10 -0
- data/sig/didit/resources/database_validation.rbs +9 -0
- data/sig/didit/resources/document_ai.rbs +9 -0
- data/sig/didit/resources/email.rbs +11 -0
- data/sig/didit/resources/face_match.rbs +9 -0
- data/sig/didit/resources/face_search.rbs +9 -0
- data/sig/didit/resources/fraud_checks.rbs +11 -0
- data/sig/didit/resources/id_verification.rbs +9 -0
- data/sig/didit/resources/id_wallets.rbs +37 -0
- data/sig/didit/resources/investigator.rbs +23 -0
- data/sig/didit/resources/ip.rbs +9 -0
- data/sig/didit/resources/kyb.rbs +12 -0
- data/sig/didit/resources/lists.rbs +20 -0
- data/sig/didit/resources/monitoring_events.rbs +16 -0
- data/sig/didit/resources/networks.rbs +25 -0
- data/sig/didit/resources/passive_liveness.rbs +9 -0
- data/sig/didit/resources/phone.rbs +11 -0
- data/sig/didit/resources/pricing.rbs +9 -0
- data/sig/didit/resources/proof_of_address.rbs +9 -0
- data/sig/didit/resources/questionnaires.rbs +13 -0
- data/sig/didit/resources/regulatory_reports.rbs +16 -0
- data/sig/didit/resources/report_templates.rbs +13 -0
- data/sig/didit/resources/sessions.rbs +78 -0
- data/sig/didit/resources/transactions.rbs +43 -0
- data/sig/didit/resources/travel_rule.rbs +62 -0
- data/sig/didit/resources/users.rbs +21 -0
- data/sig/didit/resources/wallet_screening.rbs +11 -0
- data/sig/didit/resources/webhooks.rbs +23 -0
- data/sig/didit/resources/workflows.rbs +13 -0
- data/sig/didit/resources.rbs +50 -0
- data/sig/didit/types/account.rbs +22 -0
- data/sig/didit/types/account_components.rbs +94 -0
- data/sig/didit/types/address_validation.rbs +7 -0
- data/sig/didit/types/age_estimation.rbs +80 -0
- data/sig/didit/types/aml.rbs +174 -0
- data/sig/didit/types/aml_monitoring.rbs +67 -0
- data/sig/didit/types/analytics.rbs +57 -0
- data/sig/didit/types/bank.rbs +9 -0
- data/sig/didit/types/billing.rbs +19 -0
- data/sig/didit/types/biometric_templates.rbs +268 -0
- data/sig/didit/types/blocklist.rbs +33 -0
- data/sig/didit/types/businesses.rbs +54 -0
- data/sig/didit/types/cases.rbs +414 -0
- data/sig/didit/types/components.rbs +10920 -0
- data/sig/didit/types/customization.rbs +7 -0
- data/sig/didit/types/database_validation.rbs +152 -0
- data/sig/didit/types/document_ai.rbs +123 -0
- data/sig/didit/types/email.rbs +112 -0
- data/sig/didit/types/face_match.rbs +86 -0
- data/sig/didit/types/face_search.rbs +112 -0
- data/sig/didit/types/fraud_checks.rbs +9 -0
- data/sig/didit/types/id_verification.rbs +196 -0
- data/sig/didit/types/id_wallets.rbs +55 -0
- data/sig/didit/types/investigator.rbs +212 -0
- data/sig/didit/types/ip.rbs +7 -0
- data/sig/didit/types/kyb.rbs +1254 -0
- data/sig/didit/types/lists.rbs +106 -0
- data/sig/didit/types/monitoring_events.rbs +234 -0
- data/sig/didit/types/networks.rbs +113 -0
- data/sig/didit/types/passive_liveness.rbs +87 -0
- data/sig/didit/types/phone.rbs +65 -0
- data/sig/didit/types/proof_of_address.rbs +144 -0
- data/sig/didit/types/questionnaires.rbs +30 -0
- data/sig/didit/types/regulatory_reports.rbs +101 -0
- data/sig/didit/types/report_templates.rbs +42 -0
- data/sig/didit/types/sessions.rbs +1349 -0
- data/sig/didit/types/transactions.rbs +273 -0
- data/sig/didit/types/travel_rule.rbs +188 -0
- data/sig/didit/types/users.rbs +181 -0
- data/sig/didit/types/wallet_screening.rbs +40 -0
- data/sig/didit/types/webhooks.rbs +311 -0
- data/sig/didit/types/workflows.rbs +30 -0
- data/sig/didit/webhooks/canonical_json.rbs +25 -0
- data/sig/didit/webhooks/client_methods.rbs +11 -0
- data/sig/didit/webhooks/events.rbs +125 -0
- data/sig/didit/webhooks/headers.rbs +22 -0
- data/sig/didit/webhooks/verifier.rbs +54 -0
- data/sig/didit/webhooks.rbs +37 -0
- data/sig/didit.rbs +20 -0
- metadata +300 -0
data/lib/didit/client.rb
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module Didit
|
|
7
|
+
# The Didit API client (docs/DESIGN.md section 6). Thread safe: create one per process
|
|
8
|
+
# and share it.
|
|
9
|
+
#
|
|
10
|
+
# didit = Didit::Client.new # reads DIDIT_API_KEY
|
|
11
|
+
# didit = Didit::Client.new(api_key: "...", organization_id: "...", application_id: "...")
|
|
12
|
+
# session = didit.sessions.create(workflow_id: "...", vendor_data: "user-42")
|
|
13
|
+
class Client
|
|
14
|
+
include Resources::ClientMethods
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = Internal::Config::DEFAULT_BASE_URL
|
|
17
|
+
DEFAULT_ACCOUNT_BASE_URL = Internal::Config::DEFAULT_ACCOUNT_BASE_URL
|
|
18
|
+
DEFAULT_TIMEOUT = Internal::Config::DEFAULT_TIMEOUT
|
|
19
|
+
DEFAULT_MAX_RETRIES = Internal::Config::DEFAULT_MAX_RETRIES
|
|
20
|
+
|
|
21
|
+
@deprecation_lock = Mutex.new
|
|
22
|
+
@deprecation_warned = Set.new
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# @api private
|
|
26
|
+
attr_reader :deprecation_lock, :deprecation_warned
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param api_key [String, nil] defaults to the DIDIT_API_KEY environment variable
|
|
30
|
+
# @param organization_id [String, nil] defaults to DIDIT_ORGANIZATION_ID; needed by application-scoped methods
|
|
31
|
+
# @param application_id [String, nil] defaults to DIDIT_APPLICATION_ID; needed by application-scoped methods
|
|
32
|
+
# @param base_url [String] the verification API
|
|
33
|
+
# @param account_base_url [String] the account API (+didit.account+)
|
|
34
|
+
# @param account_token [String, nil] the Bearer token of +didit.account+ methods, from +didit.account.login+
|
|
35
|
+
# @param timeout [Numeric] seconds per attempt (connect, write and read each)
|
|
36
|
+
# @param max_retries [Integer] retries after the first attempt
|
|
37
|
+
# @param default_headers [Hash{String => String}] sent with every request
|
|
38
|
+
# @param logger [#debug, nil] receives debug lines; DIDIT_LOG=debug prints them to stderr instead
|
|
39
|
+
# @param http_client [#call, nil] returns a configured, not yet started +Net::HTTP+ for a URI
|
|
40
|
+
# (proxies, CA bundle, ...); the SDK sets the timeouts and owns the connection
|
|
41
|
+
def initialize(api_key: nil, organization_id: nil, application_id: nil, base_url: nil,
|
|
42
|
+
account_base_url: nil, account_token: nil, timeout: DEFAULT_TIMEOUT,
|
|
43
|
+
max_retries: DEFAULT_MAX_RETRIES, default_headers: {}, logger: nil, http_client: nil)
|
|
44
|
+
config = Internal::Config.new(
|
|
45
|
+
api_key: api_key || ENV.fetch("DIDIT_API_KEY", nil),
|
|
46
|
+
organization_id: organization_id || ENV.fetch("DIDIT_ORGANIZATION_ID", nil),
|
|
47
|
+
application_id: application_id || ENV.fetch("DIDIT_APPLICATION_ID", nil),
|
|
48
|
+
base_url: base_url, account_base_url: account_base_url, account_token: account_token, timeout: timeout,
|
|
49
|
+
max_retries: max_retries, default_headers: default_headers, logger: logger, http_client: http_client
|
|
50
|
+
)
|
|
51
|
+
setup(config, Internal::Transport.new(http_client: config.http_client), raw: false)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [String]
|
|
55
|
+
def base_url = @config.base_url
|
|
56
|
+
# @return [String]
|
|
57
|
+
def account_base_url = @config.account_base_url
|
|
58
|
+
# @return [String, nil]
|
|
59
|
+
def organization_id = @config.organization_id
|
|
60
|
+
# @return [String, nil]
|
|
61
|
+
def application_id = @config.application_id
|
|
62
|
+
# @return [Float]
|
|
63
|
+
def timeout = @config.timeout
|
|
64
|
+
# @return [Integer]
|
|
65
|
+
def max_retries = @config.max_retries
|
|
66
|
+
|
|
67
|
+
# A client with some options changed. It shares this client's connections.
|
|
68
|
+
# Passing nil clears an id or a token.
|
|
69
|
+
#
|
|
70
|
+
# didit.with_options(timeout: 5, max_retries: 0).sessions.retrieve(id)
|
|
71
|
+
# account = didit.with_options(account_token: login.access_token)
|
|
72
|
+
#
|
|
73
|
+
# @return [Didit::Client]
|
|
74
|
+
def with_options(api_key: OMIT, account_token: OMIT, organization_id: OMIT, application_id: OMIT, base_url: OMIT,
|
|
75
|
+
account_base_url: OMIT, timeout: OMIT, max_retries: OMIT, default_headers: OMIT, logger: OMIT)
|
|
76
|
+
changes = {
|
|
77
|
+
api_key: api_key, account_token: account_token, organization_id: organization_id,
|
|
78
|
+
application_id: application_id, base_url: base_url, account_base_url: account_base_url, timeout: timeout,
|
|
79
|
+
max_retries: max_retries, default_headers: default_headers, logger: logger
|
|
80
|
+
}.reject { |_, value| value.equal?(OMIT) }
|
|
81
|
+
copy(@config.merge(changes), raw: @raw)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A client whose methods return the {Didit::RawResponse} (status, headers, request id)
|
|
85
|
+
# instead of the parsed result; +#parse+ gives the result.
|
|
86
|
+
#
|
|
87
|
+
# @return [Didit::Client]
|
|
88
|
+
def with_raw_response
|
|
89
|
+
copy(@config, raw: true)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Whether methods return {Didit::RawResponse}.
|
|
93
|
+
def raw_response?
|
|
94
|
+
@raw
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Closes the idle connections (they also close by themselves).
|
|
98
|
+
def close
|
|
99
|
+
@transport.close
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def inspect
|
|
103
|
+
"#<Didit::Client base_url=#{base_url.inspect} organization_id=#{organization_id.inspect} " \
|
|
104
|
+
"application_id=#{application_id.inspect} api_key=#{@config.api_key ? "[set]" : "nil"}>"
|
|
105
|
+
end
|
|
106
|
+
alias to_s inspect
|
|
107
|
+
|
|
108
|
+
# Sends a request to any API path; the generated resource methods call this.
|
|
109
|
+
#
|
|
110
|
+
# @param http_method [Symbol] :get, :post, :put, :patch or :delete
|
|
111
|
+
# @param path [String] a path with +{name}+ placeholders filled from +path_params+
|
|
112
|
+
# @param response [Object] the type of the result: a Didit::Model subclass, +[:array, type]+,
|
|
113
|
+
# +:binary+, a Hash of types by status, or nil for no content
|
|
114
|
+
# @param page [Object] the item type of a paginated list (returns a {Didit::Page})
|
|
115
|
+
# @return [Object] the typed result, or a {Didit::RawResponse} from {#with_raw_response}
|
|
116
|
+
def request(http_method, path, path_params: {}, query: nil, body: nil, encoding: :json, file_fields: [],
|
|
117
|
+
headers: {}, auth: :api_key, api: :verification, scope: nil, response: nil, page: nil,
|
|
118
|
+
accept: "application/json", request_options: nil, &block)
|
|
119
|
+
opts = Internal::Request.options(@config, request_options)
|
|
120
|
+
prepared = Internal::Request.prepare(
|
|
121
|
+
@config, http_method, path, opts,
|
|
122
|
+
path_params: path_params, query: query, body: body, encoding: encoding, file_fields: file_fields,
|
|
123
|
+
headers: headers, auth: auth, api: api, scope: scope, accept: accept
|
|
124
|
+
)
|
|
125
|
+
stream = response == :binary ? block : nil
|
|
126
|
+
result = Internal::Request.perform(@config, @transport, prepared, &stream)
|
|
127
|
+
parser = -> { parse(prepared, result.response, response, page) }
|
|
128
|
+
return parser.call unless @raw
|
|
129
|
+
|
|
130
|
+
raw = check_status(prepared, result.response)
|
|
131
|
+
RawResponse.new(status: raw.status, headers: raw.headers, body: raw.body,
|
|
132
|
+
retries_taken: result.retries_taken, elapsed: result.elapsed, parser: parser)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Fetches a page's +next+ link with the request options of the first page.
|
|
136
|
+
# @api private
|
|
137
|
+
def fetch_page(prepared, uri, item, seen)
|
|
138
|
+
next_request = prepared.dup
|
|
139
|
+
next_request.uri = uri
|
|
140
|
+
next_request.http_method = :get
|
|
141
|
+
next_request.body = nil
|
|
142
|
+
result = Internal::Request.perform(@config, @transport, next_request)
|
|
143
|
+
response = check_status(next_request, result.response)
|
|
144
|
+
Page.new(client: self, prepared: next_request, item: item, data: parse_json(next_request, response),
|
|
145
|
+
seen: seen)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Warns once per process that a deprecated method was called (Ruby's :deprecated warning category).
|
|
149
|
+
# @api private
|
|
150
|
+
def deprecated(name, note)
|
|
151
|
+
first = Client.deprecation_lock.synchronize { Client.deprecation_warned.add?(name) }
|
|
152
|
+
Kernel.warn("Didit: #{name} is deprecated. #{note}", category: :deprecated) if first
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
protected
|
|
157
|
+
|
|
158
|
+
attr_reader :config, :transport
|
|
159
|
+
|
|
160
|
+
def setup(config, transport, raw:)
|
|
161
|
+
@config = config
|
|
162
|
+
@transport = transport
|
|
163
|
+
@raw = raw
|
|
164
|
+
self
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def copy(config, raw:)
|
|
170
|
+
same = config.http_client.equal?(@config.http_client)
|
|
171
|
+
transport = same ? @transport : Internal::Transport.new(http_client: config.http_client)
|
|
172
|
+
Client.allocate.send(:setup, config, transport, raw: raw)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def parse(prepared, response, type, page)
|
|
176
|
+
check_status(prepared, response)
|
|
177
|
+
return Page.new(client: self, prepared: prepared, item: page, data: parse_json(prepared, response)) if page
|
|
178
|
+
return response.body if type == :binary
|
|
179
|
+
return nil if type.nil?
|
|
180
|
+
|
|
181
|
+
type = type.fetch(response.status) { type.values.first } if type.is_a?(Hash)
|
|
182
|
+
return nil if response.status == 204 || response.body.nil? || response.body.empty?
|
|
183
|
+
|
|
184
|
+
Model.coerce(parse_json(prepared, response), type)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def check_status(prepared, response)
|
|
188
|
+
return response if response.status.between?(200, 299)
|
|
189
|
+
|
|
190
|
+
raise Internal::Errors.status_error(status: response.status, headers: response.headers,
|
|
191
|
+
raw_body: response.body, redact: prepared.redact)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def parse_json(prepared, response)
|
|
195
|
+
text = response.body.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
196
|
+
begin
|
|
197
|
+
raise JSON::ParserError, "invalid UTF-8" unless text.valid_encoding?
|
|
198
|
+
|
|
199
|
+
return JSON.parse(text)
|
|
200
|
+
rescue JSON::ParserError
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
raise APIResponseValidationError.new(
|
|
204
|
+
"Expected a JSON response from #{prepared.http_method.to_s.upcase} #{prepared.uri.path}, got " \
|
|
205
|
+
"#{response.headers["content-type"] || "no content type"}.",
|
|
206
|
+
status: response.status, headers: response.headers,
|
|
207
|
+
body: Internal::Util.redact(text.scrub, prepared.redact), request_id: Internal::Util.request_id(response.headers)
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
data/lib/didit/errors.rb
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Didit
|
|
4
|
+
# Base class of every error the SDK raises (docs/DESIGN.md section 7).
|
|
5
|
+
class DiditError < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# The client is missing something it needs (an API key, an account token, the
|
|
9
|
+
# organization or application id, a webhook secret) or an argument is invalid.
|
|
10
|
+
# Raised before any request is sent.
|
|
11
|
+
class ConfigurationError < DiditError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# No response arrived: the connection failed or broke.
|
|
15
|
+
class APIConnectionError < DiditError
|
|
16
|
+
# @return [String, nil] the HTTP method of the request
|
|
17
|
+
attr_reader :http_method
|
|
18
|
+
# @return [String, nil] the request URL without its query string
|
|
19
|
+
attr_reader :url
|
|
20
|
+
|
|
21
|
+
def initialize(message = "Connection error.", http_method: nil, url: nil)
|
|
22
|
+
super(message)
|
|
23
|
+
@http_method = http_method
|
|
24
|
+
@url = url
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The request timed out before a response arrived.
|
|
29
|
+
class APITimeoutError < APIConnectionError
|
|
30
|
+
def initialize(message = "Request timed out.", http_method: nil, url: nil)
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Shared readers of the errors that carry an HTTP response.
|
|
36
|
+
module ResponseDetails
|
|
37
|
+
# @return [Integer] the HTTP status code
|
|
38
|
+
attr_reader :status
|
|
39
|
+
# @return [Hash{String => String}] response headers, names in lower case
|
|
40
|
+
attr_reader :headers
|
|
41
|
+
# @return [Object, nil] the parsed JSON body, else the text, else nil
|
|
42
|
+
attr_reader :body
|
|
43
|
+
# @return [String, nil] the request id the API returned, if any
|
|
44
|
+
attr_reader :request_id
|
|
45
|
+
|
|
46
|
+
# @return [Integer] alias of {#status}
|
|
47
|
+
def status_code
|
|
48
|
+
status
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A successful response could not be read as the documented content (for
|
|
53
|
+
# example HTML where JSON was expected, or a pagination link that loops).
|
|
54
|
+
class APIResponseValidationError < DiditError
|
|
55
|
+
include ResponseDetails
|
|
56
|
+
|
|
57
|
+
def initialize(message, status:, headers: {}, body: nil, request_id: nil)
|
|
58
|
+
super(message)
|
|
59
|
+
@status = status
|
|
60
|
+
@headers = headers
|
|
61
|
+
@body = body
|
|
62
|
+
@request_id = request_id
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The API answered with a non-2xx status.
|
|
67
|
+
class APIStatusError < DiditError
|
|
68
|
+
include ResponseDetails
|
|
69
|
+
|
|
70
|
+
def initialize(message, status:, headers: {}, body: nil, request_id: nil)
|
|
71
|
+
super(request_id ? "#{message} (request id #{request_id})" : message)
|
|
72
|
+
@status = status
|
|
73
|
+
@headers = headers
|
|
74
|
+
@body = body
|
|
75
|
+
@request_id = request_id
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# HTTP 400.
|
|
80
|
+
class BadRequestError < APIStatusError
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# HTTP 401.
|
|
84
|
+
class AuthenticationError < APIStatusError
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# HTTP 403.
|
|
88
|
+
class PermissionDeniedError < APIStatusError
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# HTTP 404.
|
|
92
|
+
class NotFoundError < APIStatusError
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# HTTP 409.
|
|
96
|
+
class ConflictError < APIStatusError
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# HTTP 422.
|
|
100
|
+
class UnprocessableEntityError < APIStatusError
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# HTTP 429.
|
|
104
|
+
class RateLimitError < APIStatusError
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# HTTP 5xx.
|
|
108
|
+
class InternalServerError < APIStatusError
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A webhook delivery failed verification (docs/DESIGN.md section 9.3).
|
|
112
|
+
#
|
|
113
|
+
# {#reason} is machine readable: +:missing_signature+, +:invalid_signature+,
|
|
114
|
+
# +:missing_timestamp+, +:timestamp_mismatch+, +:timestamp_out_of_tolerance+
|
|
115
|
+
# or +:invalid_payload+.
|
|
116
|
+
class WebhookVerificationError < DiditError
|
|
117
|
+
REASONS = %i[
|
|
118
|
+
missing_signature invalid_signature missing_timestamp
|
|
119
|
+
timestamp_mismatch timestamp_out_of_tolerance invalid_payload
|
|
120
|
+
].freeze
|
|
121
|
+
|
|
122
|
+
# @return [Symbol]
|
|
123
|
+
attr_reader :reason
|
|
124
|
+
|
|
125
|
+
def initialize(reason, message)
|
|
126
|
+
super(message)
|
|
127
|
+
@reason = reason
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# An Authentication access token or ID token failed verification: do not trust
|
|
132
|
+
# any of its claims (docs/DESIGN.md section 10). The message never contains the token.
|
|
133
|
+
#
|
|
134
|
+
# {#reason} is machine readable: +:malformed+, +:unsupported_algorithm+,
|
|
135
|
+
# +:invalid_type+, +:unknown_key+, +:invalid_signature+, +:invalid_issuer+,
|
|
136
|
+
# +:invalid_audience+, +:expired+, +:not_yet_valid+, +:invalid_issued_at+,
|
|
137
|
+
# +:invalid_nonce+ or +:missing_claim+.
|
|
138
|
+
class TokenVerificationError < DiditError
|
|
139
|
+
REASONS = %i[
|
|
140
|
+
malformed unsupported_algorithm invalid_type unknown_key invalid_signature
|
|
141
|
+
invalid_issuer invalid_audience expired not_yet_valid invalid_issued_at
|
|
142
|
+
invalid_nonce missing_claim
|
|
143
|
+
].freeze
|
|
144
|
+
|
|
145
|
+
# @return [Symbol]
|
|
146
|
+
attr_reader :reason
|
|
147
|
+
|
|
148
|
+
def initialize(reason, message)
|
|
149
|
+
super(message)
|
|
150
|
+
@reason = reason
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Didit
|
|
6
|
+
# A file to upload (docs/DESIGN.md section 8), with an optional file name and content type.
|
|
7
|
+
#
|
|
8
|
+
# Upload arguments accept a +Pathname+ (read from disk), an +IO+ such as +File+ or
|
|
9
|
+
# +StringIO+ (read once), a +String+ holding the file's bytes, or a FilePart:
|
|
10
|
+
#
|
|
11
|
+
# didit.id_verification.verify(front_image: Pathname("passport.jpg"))
|
|
12
|
+
# didit.id_verification.verify(front_image: File.open("passport.jpg", "rb"))
|
|
13
|
+
# didit.id_verification.verify(front_image: Didit::FilePart.new(bytes, filename: "front.png"))
|
|
14
|
+
class FilePart
|
|
15
|
+
# File types by extension. A fixed table, so uploads do not depend on the host's MIME database.
|
|
16
|
+
CONTENT_TYPES = {
|
|
17
|
+
".bmp" => "image/bmp", ".csv" => "text/csv", ".gif" => "image/gif", ".heic" => "image/heic",
|
|
18
|
+
".heif" => "image/heif", ".jpeg" => "image/jpeg", ".jpg" => "image/jpeg", ".json" => "application/json",
|
|
19
|
+
".mov" => "video/quicktime", ".mp4" => "video/mp4", ".pdf" => "application/pdf", ".png" => "image/png",
|
|
20
|
+
".tif" => "image/tiff", ".tiff" => "image/tiff", ".txt" => "text/plain", ".webm" => "video/webm",
|
|
21
|
+
".webp" => "image/webp", ".zip" => "application/zip"
|
|
22
|
+
}.freeze
|
|
23
|
+
DEFAULT_CONTENT_TYPE = "application/octet-stream"
|
|
24
|
+
|
|
25
|
+
# @return [String, Pathname, IO]
|
|
26
|
+
attr_reader :content
|
|
27
|
+
# @return [String, nil]
|
|
28
|
+
attr_reader :filename
|
|
29
|
+
|
|
30
|
+
# @param content [String, Pathname, IO] the bytes, a path to read, or an IO to read
|
|
31
|
+
# @param filename [String, nil] defaults to the path's base name
|
|
32
|
+
# @param content_type [String, nil] defaults to a type guessed from the file name
|
|
33
|
+
def initialize(content, filename: nil, content_type: nil)
|
|
34
|
+
unless content.is_a?(String) || content.is_a?(Pathname) || content.respond_to?(:read)
|
|
35
|
+
raise ConfigurationError, "A file must be a Pathname, an IO or a String of bytes, got #{content.class}."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@content = content
|
|
39
|
+
@filename = filename || FilePart.default_filename(content)
|
|
40
|
+
@content_type = content_type
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A FilePart for a path on disk.
|
|
44
|
+
def self.from_path(path, filename: nil, content_type: nil)
|
|
45
|
+
new(Pathname(path), filename: filename, content_type: content_type)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Wraps an upload argument; +field+ names the file when nothing else does.
|
|
49
|
+
# @api private
|
|
50
|
+
def self.wrap(value, field)
|
|
51
|
+
part = value.is_a?(FilePart) ? value : new(value)
|
|
52
|
+
part.filename ? part : new(part.content, filename: field, content_type: part.content_type)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @api private
|
|
56
|
+
def self.default_filename(content)
|
|
57
|
+
path = if content.is_a?(Pathname)
|
|
58
|
+
content
|
|
59
|
+
elsif content.respond_to?(:path) && content.path
|
|
60
|
+
content.path
|
|
61
|
+
end
|
|
62
|
+
path ? File.basename(path.to_s) : nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [String]
|
|
66
|
+
def content_type
|
|
67
|
+
@content_type || CONTENT_TYPES.fetch(File.extname(filename.to_s).downcase, DEFAULT_CONTENT_TYPE)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The file's bytes (an IO is read once, from its current position).
|
|
71
|
+
# @return [String]
|
|
72
|
+
def read
|
|
73
|
+
data = case content
|
|
74
|
+
when Pathname then File.binread(content.to_s)
|
|
75
|
+
when String then content
|
|
76
|
+
else content.read
|
|
77
|
+
end
|
|
78
|
+
data.to_s.b
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def inspect
|
|
82
|
+
"#<Didit::FilePart filename=#{filename.inspect} content_type=#{content_type.inspect}>"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Didit
|
|
6
|
+
module Internal
|
|
7
|
+
# The resolved, immutable options of a Didit::Client (docs/DESIGN.md section 6).
|
|
8
|
+
# @api private
|
|
9
|
+
class Config
|
|
10
|
+
DEFAULT_BASE_URL = "https://verification.didit.me"
|
|
11
|
+
DEFAULT_ACCOUNT_BASE_URL = "https://apx.didit.me/auth/v2"
|
|
12
|
+
DEFAULT_TIMEOUT = 60.0
|
|
13
|
+
DEFAULT_MAX_RETRIES = 2
|
|
14
|
+
FIELDS = %i[
|
|
15
|
+
api_key account_token organization_id application_id base_url account_base_url timeout max_retries
|
|
16
|
+
default_headers logger http_client
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader(*FIELDS)
|
|
20
|
+
|
|
21
|
+
def initialize(**options)
|
|
22
|
+
unknown = options.keys - FIELDS
|
|
23
|
+
raise ConfigurationError, "Unknown client options #{unknown.map(&:inspect).join(", ")}." unless unknown.empty?
|
|
24
|
+
|
|
25
|
+
@api_key, @account_token, @organization_id, @application_id =
|
|
26
|
+
options.values_at(:api_key, :account_token, :organization_id, :application_id).map { |v| present(v) }
|
|
27
|
+
@base_url = Config.check_url(:base_url, options[:base_url] || DEFAULT_BASE_URL)
|
|
28
|
+
@account_base_url = Config.check_url(:account_base_url, options[:account_base_url] || DEFAULT_ACCOUNT_BASE_URL)
|
|
29
|
+
@timeout = Config.check_timeout(options.fetch(:timeout, DEFAULT_TIMEOUT))
|
|
30
|
+
@max_retries = Config.check_max_retries(options.fetch(:max_retries, DEFAULT_MAX_RETRIES))
|
|
31
|
+
@default_headers = check_headers(options[:default_headers] || {})
|
|
32
|
+
@logger = options[:logger]
|
|
33
|
+
@http_client = check_http_client(options[:http_client])
|
|
34
|
+
freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A copy with +changes+ applied; a nil value clears an id or token.
|
|
38
|
+
def merge(changes)
|
|
39
|
+
Config.new(**FIELDS.to_h { |f| [f, public_send(f)] }, **changes)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.check_url(name, url)
|
|
43
|
+
uri = URI(url.to_s)
|
|
44
|
+
unless %w[http https].include?(uri.scheme) && uri.host && !uri.host.empty? && uri.query.nil? &&
|
|
45
|
+
uri.fragment.nil? && uri.userinfo.nil?
|
|
46
|
+
raise ConfigurationError, "#{name} must be an absolute http(s) URL without query or credentials, " \
|
|
47
|
+
"got #{url.to_s.inspect}."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
uri.to_s.sub(%r{/+\z}, "")
|
|
51
|
+
rescue URI::InvalidURIError
|
|
52
|
+
raise ConfigurationError, "#{name} must be an absolute http(s) URL, got #{url.to_s.inspect}."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.check_timeout(value)
|
|
56
|
+
unless value.is_a?(Numeric) && value.positive? && value.finite?
|
|
57
|
+
raise ConfigurationError, "timeout must be a positive number of seconds, got #{value.inspect}."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
(_ = value).to_f
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.check_max_retries(value)
|
|
64
|
+
unless value.is_a?(Integer) && !value.negative?
|
|
65
|
+
raise ConfigurationError, "max_retries must be an Integer of 0 or more, got #{value.inspect}."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def present(value)
|
|
74
|
+
return nil if value.nil?
|
|
75
|
+
|
|
76
|
+
text = value.to_s.strip
|
|
77
|
+
text.empty? ? nil : text
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def check_headers(headers)
|
|
81
|
+
raise ConfigurationError, "default_headers must be a Hash." unless headers.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
headers.to_h { |k, v| [k.to_s, v&.to_s] }.freeze
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def check_http_client(value)
|
|
87
|
+
return nil if value.nil?
|
|
88
|
+
return value if value.respond_to?(:call)
|
|
89
|
+
|
|
90
|
+
raise ConfigurationError, "http_client must respond to call(uri) and return a Net::HTTP."
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Didit
|
|
6
|
+
module Internal
|
|
7
|
+
# Builds {Didit::APIStatusError} subclasses from responses.
|
|
8
|
+
# @api private
|
|
9
|
+
module Errors
|
|
10
|
+
STATUS_ERRORS = {
|
|
11
|
+
400 => BadRequestError,
|
|
12
|
+
401 => AuthenticationError,
|
|
13
|
+
403 => PermissionDeniedError,
|
|
14
|
+
404 => NotFoundError,
|
|
15
|
+
409 => ConflictError,
|
|
16
|
+
422 => UnprocessableEntityError,
|
|
17
|
+
429 => RateLimitError
|
|
18
|
+
}.freeze
|
|
19
|
+
DETAIL_KEYS = %w[detail message error error_description].freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# @param status [Integer]
|
|
24
|
+
# @param headers [Hash{String => String}] names in lower case
|
|
25
|
+
# @param raw_body [String, nil]
|
|
26
|
+
# @param redact [Array<String, nil>] secrets to replace with "[redacted]" in the message and body
|
|
27
|
+
# @return [Didit::APIStatusError]
|
|
28
|
+
def status_error(status:, headers:, raw_body:, redact: [])
|
|
29
|
+
body = Util.redact(parse_body(raw_body), redact)
|
|
30
|
+
message = Util.redact(message_for(status, body), redact)
|
|
31
|
+
klass = STATUS_ERRORS.fetch(status) { status >= 500 ? InternalServerError : APIStatusError }
|
|
32
|
+
klass.new(message, status: status, headers: headers, body: body, request_id: Util.request_id(headers))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The parsed JSON body, else the text, else nil.
|
|
36
|
+
def parse_body(raw_body)
|
|
37
|
+
return nil if raw_body.nil? || raw_body.empty?
|
|
38
|
+
|
|
39
|
+
text = raw_body.dup.force_encoding(Encoding::UTF_8)
|
|
40
|
+
text = text.scrub unless text.valid_encoding?
|
|
41
|
+
JSON.parse(text)
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
text
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def message_for(status, body)
|
|
47
|
+
detail = detail_of(body)
|
|
48
|
+
detail ? "HTTP #{status}: #{detail}" : "HTTP #{status}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def detail_of(body)
|
|
52
|
+
case body
|
|
53
|
+
when Hash then hash_detail(body)
|
|
54
|
+
when Array then body.join("; ") if !body.empty? && body.all?(String)
|
|
55
|
+
when String
|
|
56
|
+
text = body.strip
|
|
57
|
+
text unless text.empty? || text.length > 500
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def hash_detail(body)
|
|
62
|
+
error = body["error"]
|
|
63
|
+
description = body["error_description"]
|
|
64
|
+
# OAuth 2.0 errors (RFC 6749 section 5.2): the code and what it means.
|
|
65
|
+
return "#{error}: #{description}" if present?(error) && present?(description)
|
|
66
|
+
|
|
67
|
+
DETAIL_KEYS.each { |key| return body[key] if present?(body[key]) }
|
|
68
|
+
parts = body.first(5).filter_map do |key, value|
|
|
69
|
+
if value.is_a?(Array) && value.first.is_a?(String)
|
|
70
|
+
"#{key}: #{value.first}"
|
|
71
|
+
elsif value.is_a?(String)
|
|
72
|
+
"#{key}: #{value}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
parts.join("; ") unless parts.empty?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def present?(value)
|
|
79
|
+
value.is_a?(String) && !value.empty?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Didit
|
|
4
|
+
module Internal
|
|
5
|
+
# Debug logging (docs/DESIGN.md section 6): method, path, status, duration and request
|
|
6
|
+
# id, never bodies, headers or query strings.
|
|
7
|
+
# @api private
|
|
8
|
+
module Log
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def response(config, prepared, response, elapsed)
|
|
12
|
+
return unless enabled?(config)
|
|
13
|
+
|
|
14
|
+
request_id = Util.request_id(response.headers) || "-"
|
|
15
|
+
write(config, format("%<method>s %<path>s -> %<status>d in %<ms>.0f ms (request id %<id>s)",
|
|
16
|
+
method: prepared.http_method.to_s.upcase, path: prepared.uri.path,
|
|
17
|
+
status: response.status, ms: elapsed * 1000, id: request_id))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def retry(config, prepared, reason, delay, retries_left)
|
|
21
|
+
return unless enabled?(config)
|
|
22
|
+
|
|
23
|
+
write(config, format("%<method>s %<path>s: %<reason>s; retrying in %<delay>.2f s (%<left>d retries left)",
|
|
24
|
+
method: prepared.http_method.to_s.upcase, path: prepared.uri.path, reason: reason,
|
|
25
|
+
delay: delay, left: retries_left))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def enabled?(config)
|
|
29
|
+
!config.logger.nil? || Util.debug_from_env?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def write(config, message)
|
|
33
|
+
if config.logger
|
|
34
|
+
config.logger.debug("didit") { message }
|
|
35
|
+
else
|
|
36
|
+
$stderr.puts("[didit] #{message}") # rubocop:disable Style/StderrPuts
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|