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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Didit
|
|
7
|
+
module Internal
|
|
8
|
+
# multipart/form-data bodies. Files are read into memory, so a retry can send them again.
|
|
9
|
+
# @api private
|
|
10
|
+
module Multipart
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# @return [Array(String, String)] the body and its content type
|
|
14
|
+
def encode(fields, file_fields)
|
|
15
|
+
boundary = "didit-#{SecureRandom.hex(16)}"
|
|
16
|
+
body = +"".b
|
|
17
|
+
fields.each do |name, value|
|
|
18
|
+
next if value.nil?
|
|
19
|
+
|
|
20
|
+
if file_fields.include?(name) || value.is_a?(FilePart)
|
|
21
|
+
(value.is_a?(Array) ? value : [value]).each do |item|
|
|
22
|
+
file_part(body, boundary, name, FilePart.wrap(item, name))
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
field_part(body, boundary, name, text(value))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
body << "--#{boundary}--\r\n"
|
|
29
|
+
[body, "multipart/form-data; boundary=#{boundary}"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def text(value)
|
|
33
|
+
case value
|
|
34
|
+
when Hash, Array then JSON.generate(Request.json_value(value))
|
|
35
|
+
when Time then value.utc.iso8601(6)
|
|
36
|
+
when Date then value.iso8601
|
|
37
|
+
else value.to_s
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def field_part(body, boundary, name, value)
|
|
42
|
+
body << "--#{boundary}\r\n"
|
|
43
|
+
body << "Content-Disposition: form-data; name=\"#{quote(name)}\"\r\n\r\n"
|
|
44
|
+
body << value.to_s.b << "\r\n"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def file_part(body, boundary, name, part)
|
|
48
|
+
body << "--#{boundary}\r\n"
|
|
49
|
+
body << "Content-Disposition: form-data; name=\"#{quote(name)}\"; filename=\"#{quote(part.filename)}\"\r\n"
|
|
50
|
+
body << "Content-Type: #{part.content_type}\r\n\r\n"
|
|
51
|
+
body << part.read << "\r\n"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Escapes a name for a Content-Disposition parameter (WHATWG multipart rules).
|
|
55
|
+
def quote(value)
|
|
56
|
+
value.to_s.b.gsub("\r", "%0D").gsub("\n", "%0A").gsub('"', "%22")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Didit
|
|
9
|
+
module Internal
|
|
10
|
+
# Builds requests and sends them with the retry policy of docs/DESIGN.md section 7.
|
|
11
|
+
# @api private
|
|
12
|
+
module Request
|
|
13
|
+
# A request with everything resolved.
|
|
14
|
+
Prepared = Struct.new(:http_method, :uri, :headers, :body, :timeout, :max_retries, :origin, :redact,
|
|
15
|
+
keyword_init: true)
|
|
16
|
+
# A response plus how it was obtained.
|
|
17
|
+
Result = Struct.new(:response, :retries_taken, :elapsed, keyword_init: true)
|
|
18
|
+
|
|
19
|
+
IDEMPOTENT = %i[get head put delete options].freeze
|
|
20
|
+
INITIAL_RETRY_DELAY = 0.5
|
|
21
|
+
MAX_RETRY_DELAY = 8.0
|
|
22
|
+
MAX_RETRY_AFTER = 60.0
|
|
23
|
+
OPTION_KEYS = %i[timeout max_retries extra_headers extra_query extra_body].freeze
|
|
24
|
+
UNRESERVED = /[^A-Za-z0-9\-._~]/
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# Per-request options (+request_options:+ of every method) merged over the client's.
|
|
29
|
+
def options(config, raw)
|
|
30
|
+
raw ||= {} # : Hash[untyped, untyped]
|
|
31
|
+
raise ConfigurationError, "request_options must be a Hash, got #{raw.class}." unless raw.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
unknown = raw.keys.map(&:to_sym) - OPTION_KEYS
|
|
34
|
+
unless unknown.empty?
|
|
35
|
+
raise ConfigurationError, "Unknown request_options #{unknown.map(&:inspect).join(", ")}; " \
|
|
36
|
+
"use #{OPTION_KEYS.map(&:inspect).join(", ")}."
|
|
37
|
+
end
|
|
38
|
+
opts = raw.transform_keys(&:to_sym)
|
|
39
|
+
{
|
|
40
|
+
timeout: Config.check_timeout(opts.fetch(:timeout, config.timeout)),
|
|
41
|
+
max_retries: Config.check_max_retries(opts.fetch(:max_retries, config.max_retries)),
|
|
42
|
+
extra_headers: opts[:extra_headers] || {},
|
|
43
|
+
extra_query: opts[:extra_query] || {},
|
|
44
|
+
extra_body: opts[:extra_body] || {}
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Prepared]
|
|
49
|
+
def prepare(config, http_method, path, opts, path_params: {}, query: nil, body: nil, encoding: :json,
|
|
50
|
+
file_fields: [], headers: {}, auth: :api_key, api: :verification, scope: nil,
|
|
51
|
+
accept: "application/json")
|
|
52
|
+
params = scope == :application ? scoped(config, path_params) : path_params
|
|
53
|
+
check_credentials(config, auth)
|
|
54
|
+
base = api == :account ? config.account_base_url : config.base_url
|
|
55
|
+
uri = URI("#{base}#{encode_path(path, params)}")
|
|
56
|
+
query_string = encode_query(merge(query, opts[:extra_query]))
|
|
57
|
+
uri.query = query_string unless query_string.empty?
|
|
58
|
+
body_bytes, content_type = encode_body(body, opts[:extra_body], encoding, file_fields)
|
|
59
|
+
Prepared.new(
|
|
60
|
+
http_method: http_method, uri: uri, origin: Util.origin(base),
|
|
61
|
+
headers: build_headers(config, auth, accept: accept, content_type: content_type, op_headers: headers,
|
|
62
|
+
extra_headers: opts[:extra_headers]),
|
|
63
|
+
body: body_bytes, timeout: opts[:timeout], max_retries: opts[:max_retries],
|
|
64
|
+
redact: [config.api_key, config.account_token].compact
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The path parameters plus the organization and application ids of an application-scoped method.
|
|
69
|
+
def scoped(config, path_params)
|
|
70
|
+
organization_id = config.organization_id
|
|
71
|
+
application_id = config.application_id
|
|
72
|
+
unless organization_id && application_id
|
|
73
|
+
raise ConfigurationError,
|
|
74
|
+
"This method is application scoped: pass organization_id and application_id to " \
|
|
75
|
+
"Didit::Client.new (or set DIDIT_ORGANIZATION_ID and DIDIT_APPLICATION_ID)."
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
path_params.merge("organization_id" => organization_id, "application_id" => application_id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def check_credentials(config, auth)
|
|
82
|
+
case auth
|
|
83
|
+
when :api_key
|
|
84
|
+
return if config.api_key
|
|
85
|
+
|
|
86
|
+
raise ConfigurationError,
|
|
87
|
+
"No API key: pass api_key: to Didit::Client.new or set the DIDIT_API_KEY environment variable."
|
|
88
|
+
when :account_token
|
|
89
|
+
return if config.account_token
|
|
90
|
+
|
|
91
|
+
raise ConfigurationError,
|
|
92
|
+
"This account method needs an account token: call didit.account.login(...) and pass its " \
|
|
93
|
+
"access_token as account_token: (didit.with_options(account_token: ...))."
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Fills +{name}+ placeholders with percent-encoded single path segments.
|
|
98
|
+
def encode_path(path, params)
|
|
99
|
+
path.gsub(/\{([^}]+)\}/) do
|
|
100
|
+
name = Regexp.last_match(1).to_s
|
|
101
|
+
value = params.fetch(name) { raise ConfigurationError, "Missing path parameter #{name}." }
|
|
102
|
+
segment = path_segment(name, value)
|
|
103
|
+
segment.b.gsub(UNRESERVED) { |c| format("%%%02X", c.ord) }.force_encoding(Encoding::US_ASCII)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def path_segment(name, value)
|
|
108
|
+
unless value.is_a?(String) || value.is_a?(Integer) || value.is_a?(Symbol)
|
|
109
|
+
raise ConfigurationError, "Path parameter #{name} must be a String, got #{value.class}."
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
text = value.to_s
|
|
113
|
+
raise ConfigurationError, "Path parameter #{name} must not be empty." if text.empty?
|
|
114
|
+
raise ConfigurationError, "Path parameter #{name} must not be \".\" or \"..\"." if %w[. ..].include?(text)
|
|
115
|
+
|
|
116
|
+
text.encode(Encoding::UTF_8)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def merge(values, extra)
|
|
120
|
+
# @type var out: Hash[String, untyped]
|
|
121
|
+
out = {}
|
|
122
|
+
(values || {}).each { |k, v| out[k.to_s] = v unless v.equal?(OMIT) }
|
|
123
|
+
(extra || {}).each { |k, v| out[k.to_s] = v }
|
|
124
|
+
out
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Query values: nil and omitted values are left out, arrays repeat the key.
|
|
128
|
+
def encode_query(values)
|
|
129
|
+
# @type var pairs: Array[[String, String]]
|
|
130
|
+
pairs = []
|
|
131
|
+
values.each do |key, value|
|
|
132
|
+
Array(value.is_a?(Hash) ? JSON.generate(json_value(value)) : value).each do |item|
|
|
133
|
+
pairs << [key, query_value(item)] unless item.nil? || item.equal?(OMIT)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
URI.encode_www_form(pairs)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def query_value(value)
|
|
140
|
+
case value
|
|
141
|
+
when Time then value.utc.iso8601(6)
|
|
142
|
+
when Date then value.iso8601
|
|
143
|
+
else value.to_s
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @return [Array(String, String), Array(nil, nil)] body bytes and content type
|
|
148
|
+
def encode_body(body, extra_body, encoding, file_fields)
|
|
149
|
+
return [nil, nil] if body.nil? && (extra_body.nil? || extra_body.empty?)
|
|
150
|
+
|
|
151
|
+
fields = merge(body, extra_body)
|
|
152
|
+
multipart = encoding == :multipart ||
|
|
153
|
+
(encoding == :auto && file_fields.any? { |f| !fields[f].nil? })
|
|
154
|
+
if multipart
|
|
155
|
+
Multipart.encode(fields, file_fields)
|
|
156
|
+
else
|
|
157
|
+
[JSON.generate(json_value(fields)), "application/json"]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# A value ready for JSON: string keys, Time and Date as ISO 8601.
|
|
162
|
+
def json_value(value)
|
|
163
|
+
case value
|
|
164
|
+
when Hash
|
|
165
|
+
# @type var out: Hash[String, untyped]
|
|
166
|
+
out = {}
|
|
167
|
+
value.each { |k, v| out[k.to_s] = json_value(v) unless v.equal?(OMIT) }
|
|
168
|
+
out
|
|
169
|
+
when Array then value.map { |v| json_value(v) }
|
|
170
|
+
when Time then value.utc.iso8601(6)
|
|
171
|
+
when Date then value.iso8601
|
|
172
|
+
when Symbol then value.to_s
|
|
173
|
+
when Didit::Model then json_value(value.to_h)
|
|
174
|
+
when FilePart
|
|
175
|
+
raise ConfigurationError, "A file can only be sent to an operation that accepts uploads."
|
|
176
|
+
else value
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_headers(config, auth, accept:, content_type:, op_headers:, extra_headers:)
|
|
181
|
+
# @type var pairs: Array[[untyped, untyped]]
|
|
182
|
+
pairs = [["Accept", accept], ["User-Agent", Util.user_agent], ["X-Didit-Client", Util.client_header]]
|
|
183
|
+
pairs.concat(config.default_headers.to_a)
|
|
184
|
+
credential = credential_header(config, auth)
|
|
185
|
+
pairs << credential if credential
|
|
186
|
+
pairs << ["Content-Type", content_type] if content_type
|
|
187
|
+
pairs.concat(op_headers.reject { |_, value| value.equal?(OMIT) }.to_a)
|
|
188
|
+
pairs.concat(extra_headers.to_a)
|
|
189
|
+
merge_headers(pairs)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Header names are case-insensitive: a later value replaces an earlier one whatever its
|
|
193
|
+
# case, so no header is sent twice; nil removes the header.
|
|
194
|
+
def merge_headers(pairs)
|
|
195
|
+
# @type var headers: Hash[String, String]
|
|
196
|
+
headers = {}
|
|
197
|
+
pairs.each do |name, value|
|
|
198
|
+
name = name.to_s
|
|
199
|
+
headers.delete_if { |key, _| key.casecmp?(name) }
|
|
200
|
+
headers[name] = value.to_s unless value.nil?
|
|
201
|
+
end
|
|
202
|
+
headers
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# The credential header for +auth+, sent only to the origin the request was built for.
|
|
206
|
+
def credential_header(config, auth)
|
|
207
|
+
case auth
|
|
208
|
+
when :api_key then ["x-api-key", config.api_key.to_s]
|
|
209
|
+
when :account_token then ["Authorization", "Bearer #{config.account_token}"]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# One try: the response, or the failure and whether any body chunk was already streamed.
|
|
214
|
+
Attempt = Struct.new(:response, :failure, :streamed, :elapsed, keyword_init: true)
|
|
215
|
+
|
|
216
|
+
# Sends +prepared+, retrying as docs/DESIGN.md section 7 says.
|
|
217
|
+
#
|
|
218
|
+
# @return [Result]
|
|
219
|
+
def perform(config, transport, prepared, &block)
|
|
220
|
+
retries = 0
|
|
221
|
+
loop do
|
|
222
|
+
attempt = attempt(transport, prepared, &block)
|
|
223
|
+
delay = retry_delay(config, prepared, attempt, retries)
|
|
224
|
+
if delay.nil?
|
|
225
|
+
failure = attempt.failure
|
|
226
|
+
raise connection_error(prepared, failure), cause: failure if failure
|
|
227
|
+
|
|
228
|
+
response = attempt.response or raise Transport::Failure.new(IOError.new("no response"), sent: true)
|
|
229
|
+
return Result.new(response: response, retries_taken: retries, elapsed: attempt.elapsed)
|
|
230
|
+
end
|
|
231
|
+
retries += 1
|
|
232
|
+
Util.pause(delay)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def attempt(transport, prepared)
|
|
237
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
238
|
+
result = Attempt.new(streamed: false)
|
|
239
|
+
# @type var stream: (^(String) -> void)?
|
|
240
|
+
stream = nil
|
|
241
|
+
if block_given?
|
|
242
|
+
stream = lambda do |chunk|
|
|
243
|
+
result.streamed = true
|
|
244
|
+
yield(chunk)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
begin
|
|
248
|
+
result.response = transport.execute(prepared.http_method, prepared.uri, prepared.headers, prepared.body,
|
|
249
|
+
prepared.timeout, &stream)
|
|
250
|
+
rescue Transport::Failure => e
|
|
251
|
+
result.failure = e
|
|
252
|
+
end
|
|
253
|
+
result.elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
254
|
+
result
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Seconds to wait before the next try, or nil when this attempt is final.
|
|
258
|
+
def retry_delay(config, prepared, attempt, retries)
|
|
259
|
+
left = prepared.max_retries - retries
|
|
260
|
+
if (failure = attempt.failure)
|
|
261
|
+
return nil if attempt.streamed || left <= 0 || !retry_failure?(prepared.http_method, failure)
|
|
262
|
+
|
|
263
|
+
delay = backoff(retries)
|
|
264
|
+
Log.retry(config, prepared, failure.timeout? ? "timed out" : failure.error.class.name, delay, left)
|
|
265
|
+
return delay
|
|
266
|
+
end
|
|
267
|
+
response = attempt.response
|
|
268
|
+
return nil unless response
|
|
269
|
+
|
|
270
|
+
Log.response(config, prepared, response, attempt.elapsed)
|
|
271
|
+
return nil if left <= 0 || !retry_status?(prepared.http_method, response.status)
|
|
272
|
+
|
|
273
|
+
delay = retry_after(response.headers["retry-after"]) || backoff(retries)
|
|
274
|
+
Log.retry(config, prepared, "HTTP #{response.status}", delay, left)
|
|
275
|
+
delay
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def retry_status?(http_method, status)
|
|
279
|
+
return true if status == 429
|
|
280
|
+
return false unless IDEMPOTENT.include?(http_method)
|
|
281
|
+
|
|
282
|
+
status == 408 || status.between?(500, 599)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def retry_failure?(http_method, failure)
|
|
286
|
+
!failure.sent || IDEMPOTENT.include?(http_method)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Exponential backoff from 0.5 s with full jitter, capped at 8 s.
|
|
290
|
+
def backoff(retries_taken)
|
|
291
|
+
Kernel.rand * [MAX_RETRY_DELAY, INITIAL_RETRY_DELAY * (2**retries_taken)].min
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Seconds to wait from a Retry-After header (seconds or HTTP date), capped at 60.
|
|
295
|
+
def retry_after(value, now: Time.now)
|
|
296
|
+
return nil if value.nil?
|
|
297
|
+
|
|
298
|
+
text = value.strip
|
|
299
|
+
seconds = text.match?(/\A\d+(\.\d+)?\z/) ? text.to_f : http_date_seconds(text, now)
|
|
300
|
+
seconds&.clamp(0.0, MAX_RETRY_AFTER)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def http_date_seconds(text, now)
|
|
304
|
+
Time.httpdate(text) - now
|
|
305
|
+
rescue ArgumentError
|
|
306
|
+
nil
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def connection_error(prepared, failure)
|
|
310
|
+
url = Util.display_url(prepared.uri)
|
|
311
|
+
method = prepared.http_method.to_s.upcase
|
|
312
|
+
if failure.timeout?
|
|
313
|
+
APITimeoutError.new("Request timed out (#{method} #{url}).", http_method: method, url: url)
|
|
314
|
+
else
|
|
315
|
+
detail = Util.redact(failure.error.message.to_s, prepared.redact)
|
|
316
|
+
APIConnectionError.new("Connection error (#{method} #{url}): #{failure.error.class}: #{detail}",
|
|
317
|
+
http_method: method, url: url)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "zlib"
|
|
7
|
+
|
|
8
|
+
module Didit
|
|
9
|
+
module Internal
|
|
10
|
+
# Sends HTTP requests over persistent net/http connections, one small pool per origin.
|
|
11
|
+
# Thread safe and fork safe: a child process never reuses its parent's sockets.
|
|
12
|
+
# @api private
|
|
13
|
+
class Transport
|
|
14
|
+
# What came back: status, headers (lower-case names) and the body bytes.
|
|
15
|
+
Response = Struct.new(:status, :headers, :body, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
# Raised by {#execute} when no response arrived; +sent+ says whether the request may
|
|
18
|
+
# have reached the server.
|
|
19
|
+
class Failure < StandardError
|
|
20
|
+
attr_reader :error, :sent
|
|
21
|
+
|
|
22
|
+
def initialize(error, sent:)
|
|
23
|
+
super(error.message)
|
|
24
|
+
@error = error
|
|
25
|
+
@sent = sent
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def timeout?
|
|
29
|
+
TIMEOUT_ERRORS.any? { |klass| error.is_a?(klass) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Errors that happen before any byte of the request leaves this process.
|
|
34
|
+
NOT_SENT_ERRORS = [
|
|
35
|
+
Net::OpenTimeout, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::EADDRNOTAVAIL,
|
|
36
|
+
SocketError
|
|
37
|
+
].freeze
|
|
38
|
+
# Errors after which the request may have reached the server.
|
|
39
|
+
TRANSPORT_ERRORS = [
|
|
40
|
+
Net::ReadTimeout, Net::WriteTimeout, Timeout::Error, EOFError, IOError, SystemCallError,
|
|
41
|
+
OpenSSL::SSL::SSLError, Net::HTTPBadResponse, Net::ProtocolError, Zlib::Error
|
|
42
|
+
].freeze
|
|
43
|
+
TIMEOUT_ERRORS = [Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Timeout::Error, Errno::ETIMEDOUT].freeze
|
|
44
|
+
MAX_IDLE = 8
|
|
45
|
+
|
|
46
|
+
METHODS = {
|
|
47
|
+
get: Net::HTTP::Get, post: Net::HTTP::Post, put: Net::HTTP::Put, patch: Net::HTTP::Patch,
|
|
48
|
+
delete: Net::HTTP::Delete, head: Net::HTTP::Head, options: Net::HTTP::Options
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# @param http_client [#call, nil] returns a configured, not yet started Net::HTTP for a URI
|
|
52
|
+
def initialize(http_client: nil)
|
|
53
|
+
@http_client = http_client
|
|
54
|
+
@mutex = Mutex.new
|
|
55
|
+
@idle = {}
|
|
56
|
+
@pid = Process.pid
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attr_reader :http_client
|
|
60
|
+
|
|
61
|
+
# Sends one request. With a block, a 2xx body is yielded in chunks instead of returned.
|
|
62
|
+
#
|
|
63
|
+
# @return [Response]
|
|
64
|
+
# @raise [Failure]
|
|
65
|
+
def execute(http_method, uri, headers, body, timeout, &)
|
|
66
|
+
http = checkout(uri, timeout)
|
|
67
|
+
reusable = false
|
|
68
|
+
begin
|
|
69
|
+
request = build(http_method, uri, headers, body)
|
|
70
|
+
response = perform(http, request, &)
|
|
71
|
+
reusable = response.headers["connection"]&.casecmp?("close") != true
|
|
72
|
+
response
|
|
73
|
+
rescue *NOT_SENT_ERRORS => e
|
|
74
|
+
raise Failure.new(e, sent: false)
|
|
75
|
+
rescue *TRANSPORT_ERRORS => e
|
|
76
|
+
raise Failure.new(e, sent: true)
|
|
77
|
+
ensure
|
|
78
|
+
reusable ? checkin(uri, http) : discard(http)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Closes every idle connection.
|
|
83
|
+
def close
|
|
84
|
+
sessions = @mutex.synchronize do
|
|
85
|
+
all = @idle.values.flatten
|
|
86
|
+
@idle = {}
|
|
87
|
+
all
|
|
88
|
+
end
|
|
89
|
+
sessions.each { |http| discard(http) }
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def perform(http, request, &block)
|
|
96
|
+
# @type var result: Response?
|
|
97
|
+
result = nil
|
|
98
|
+
http.request(request) do |res|
|
|
99
|
+
# @type var headers: Hash[String, String]
|
|
100
|
+
headers = {}
|
|
101
|
+
res.each_header { |name, value| headers[name.downcase] = value }
|
|
102
|
+
status = res.code.to_i
|
|
103
|
+
if block && status.between?(200, 299)
|
|
104
|
+
res.read_body(&block)
|
|
105
|
+
result = Response.new(status: status, headers: headers, body: nil)
|
|
106
|
+
else
|
|
107
|
+
result = Response.new(status: status, headers: headers, body: res.read_body.to_s.b)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
result || raise(Net::HTTPBadResponse, "no response")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def build(http_method, uri, headers, body)
|
|
114
|
+
klass = METHODS.fetch(http_method)
|
|
115
|
+
request = klass.new(target(uri), headers)
|
|
116
|
+
request.body = body if body
|
|
117
|
+
request
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The request target: path ("/" when empty) and query.
|
|
121
|
+
def target(uri)
|
|
122
|
+
path = uri.path.to_s.empty? ? "/" : uri.path.to_s
|
|
123
|
+
uri.query ? "#{path}?#{uri.query}" : path
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def checkout(uri, timeout)
|
|
127
|
+
key = pool_key(uri)
|
|
128
|
+
http = @mutex.synchronize do
|
|
129
|
+
reset_after_fork
|
|
130
|
+
@idle[key]&.pop
|
|
131
|
+
end
|
|
132
|
+
http ||= new_session(uri)
|
|
133
|
+
configure(http, timeout)
|
|
134
|
+
start(http) unless http.started?
|
|
135
|
+
http
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def start(http)
|
|
139
|
+
http.start
|
|
140
|
+
rescue *NOT_SENT_ERRORS, *TRANSPORT_ERRORS => e
|
|
141
|
+
# Nothing was sent: the connection itself failed.
|
|
142
|
+
discard(http)
|
|
143
|
+
raise Failure.new(e, sent: false)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def checkin(uri, http)
|
|
147
|
+
keep = @mutex.synchronize do
|
|
148
|
+
list = (@idle[pool_key(uri)] ||= [])
|
|
149
|
+
if @pid == Process.pid && list.size < MAX_IDLE
|
|
150
|
+
list.push(http)
|
|
151
|
+
true
|
|
152
|
+
else
|
|
153
|
+
false
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
discard(http) unless keep
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def discard(http)
|
|
160
|
+
http.finish if http.started?
|
|
161
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def reset_after_fork
|
|
166
|
+
return if @pid == Process.pid
|
|
167
|
+
|
|
168
|
+
# The sockets belong to the parent process: forget them without closing them.
|
|
169
|
+
@idle = {}
|
|
170
|
+
@pid = Process.pid
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def new_session(uri)
|
|
174
|
+
factory = @http_client
|
|
175
|
+
http = factory ? factory.call(uri) : Net::HTTP.new(uri.hostname.to_s, uri.port)
|
|
176
|
+
unless http.is_a?(Net::HTTP)
|
|
177
|
+
raise ConfigurationError, "http_client must return a Net::HTTP instance, got #{http.class}."
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
http.use_ssl = true if uri.scheme == "https" && !http.use_ssl?
|
|
181
|
+
http
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def configure(http, timeout)
|
|
185
|
+
http.open_timeout = timeout
|
|
186
|
+
http.read_timeout = timeout
|
|
187
|
+
http.write_timeout = timeout
|
|
188
|
+
http.ssl_timeout = timeout if http.use_ssl?
|
|
189
|
+
# The SDK decides about retries (docs/DESIGN.md section 7), net/http never does.
|
|
190
|
+
http.max_retries = 0
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def pool_key(uri)
|
|
194
|
+
"#{uri.scheme}://#{uri.host}:#{uri.port}"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Didit
|
|
6
|
+
module Internal
|
|
7
|
+
# Small helpers shared by the client, webhooks and Authentication.
|
|
8
|
+
# @api private
|
|
9
|
+
module Util
|
|
10
|
+
REQUEST_ID_HEADERS = %w[x-request-id request-id x-amzn-requestid x-correlation-id].freeze
|
|
11
|
+
REDACTED = "[redacted]"
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# +User-Agent: didit-ruby/<version> (<engine> <version>; <platform>)+ (docs/DESIGN.md section 6).
|
|
16
|
+
def user_agent
|
|
17
|
+
"didit-ruby/#{Didit::VERSION} (#{RUBY_ENGINE} #{RUBY_ENGINE_VERSION}; #{RUBY_PLATFORM})"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# +X-Didit-Client: ruby/<version>+.
|
|
21
|
+
def client_header
|
|
22
|
+
"ruby/#{Didit::VERSION}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The request id from response headers (names in lower case), if any.
|
|
26
|
+
def request_id(headers)
|
|
27
|
+
REQUEST_ID_HEADERS.each do |name|
|
|
28
|
+
value = headers[name]
|
|
29
|
+
return value unless value.nil? || value.empty?
|
|
30
|
+
end
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Replaces every secret in +value+ (a String, or Hash/Array of them) with "[redacted]".
|
|
35
|
+
def redact(value, secrets)
|
|
36
|
+
secrets = secrets.compact.reject(&:empty?)
|
|
37
|
+
return value if secrets.empty?
|
|
38
|
+
|
|
39
|
+
redact_value(value, secrets)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def redact_value(value, secrets)
|
|
43
|
+
case value
|
|
44
|
+
when String then secrets.reduce(value) { |text, secret| text.gsub(secret, REDACTED) }
|
|
45
|
+
when Hash then value.to_h { |k, v| [redact_value(k, secrets), redact_value(v, secrets)] }
|
|
46
|
+
when Array then value.map { |v| redact_value(v, secrets) }
|
|
47
|
+
else value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# +scheme://host[:port]+ with the default port left out.
|
|
52
|
+
def origin(uri)
|
|
53
|
+
uri = URI(uri) unless uri.is_a?(URI::Generic)
|
|
54
|
+
default = { "http" => 80, "https" => 443 }[uri.scheme]
|
|
55
|
+
port = uri.port.nil? || uri.port == default ? "" : ":#{uri.port}"
|
|
56
|
+
"#{uri.scheme}://#{uri.host&.downcase}#{port}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The URL without query string, fragment or credentials (for errors and logs).
|
|
60
|
+
def display_url(uri)
|
|
61
|
+
uri = URI(uri) unless uri.is_a?(URI::Generic)
|
|
62
|
+
"#{origin(uri)}#{uri.path}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Sleeps between retries (a separate method, so tests can skip the wait).
|
|
66
|
+
def pause(seconds)
|
|
67
|
+
Kernel.sleep(seconds) if seconds.positive?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Whether the environment variable asks for debug logging.
|
|
71
|
+
def debug_from_env?
|
|
72
|
+
ENV.fetch("DIDIT_LOG", "").strip.casecmp?("debug")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Didit
|
|
4
|
+
# Implementation details. Nothing under this module is public API.
|
|
5
|
+
# @api private
|
|
6
|
+
module Internal
|
|
7
|
+
autoload :Config, "didit/internal/config"
|
|
8
|
+
autoload :Errors, "didit/internal/errors"
|
|
9
|
+
autoload :Log, "didit/internal/log"
|
|
10
|
+
autoload :Multipart, "didit/internal/multipart"
|
|
11
|
+
autoload :Request, "didit/internal/request"
|
|
12
|
+
autoload :Transport, "didit/internal/transport"
|
|
13
|
+
autoload :Util, "didit/internal/util"
|
|
14
|
+
end
|
|
15
|
+
end
|