dadata-rb 3.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/.rubocop.yml +134 -0
- data/CHANGELOG.md +129 -0
- data/CONTRIBUTING.md +153 -0
- data/LICENSE.txt +21 -0
- data/README.md +603 -0
- data/Rakefile +16 -0
- data/dadata.gemspec +46 -0
- data/lib/dadata/api_exceptions.rb +35 -0
- data/lib/dadata/client/base.rb +272 -0
- data/lib/dadata/client/clean.rb +52 -0
- data/lib/dadata/client/profile.rb +51 -0
- data/lib/dadata/client/suggest.rb +86 -0
- data/lib/dadata/sensitive_data.rb +58 -0
- data/lib/dadata/version.rb +5 -0
- data/lib/dadata-rb.rb +6 -0
- data/lib/dadata.rb +421 -0
- data/lib/generators/dadata/USAGE +73 -0
- data/lib/generators/dadata/initializer_generator.rb +86 -0
- data/lib/generators/dadata/templates/dadata.rb.tt +24 -0
- data/lib/generators/dadata/templates/dadata_credentials.rb.tt +48 -0
- metadata +132 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dadata
|
|
4
|
+
# Базовый класс обработки ошибок
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
class ConfigurationError < Error; end
|
|
8
|
+
|
|
9
|
+
# Raised when the API responds with a non-success HTTP status.
|
|
10
|
+
class ApiError < Error
|
|
11
|
+
attr_reader :status
|
|
12
|
+
|
|
13
|
+
def initialize(status, message)
|
|
14
|
+
@status = status
|
|
15
|
+
super("Error: #{status} - #{message}")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Raised when the request never reaches a usable response (network failure).
|
|
20
|
+
# Carries its message through StandardError, so `raise ConnectionError, 'msg'` works.
|
|
21
|
+
class ConnectionError < Error; end
|
|
22
|
+
|
|
23
|
+
# Raised when the request exceeds the configured timeout. A ConnectionError so
|
|
24
|
+
# callers that rescue ConnectionError keep working; rescue TimeoutError first
|
|
25
|
+
# when distinct handling is needed.
|
|
26
|
+
class TimeoutError < ConnectionError; end
|
|
27
|
+
|
|
28
|
+
# Status-specific API errors. The HTTP status => class mapping lives in
|
|
29
|
+
# Dadata::ClientBase::STATUS_ERRORS (400, 401, 403, 404, 429 respectively).
|
|
30
|
+
class BadRequestError < ApiError; end
|
|
31
|
+
class UnauthorizedError < ApiError; end
|
|
32
|
+
class AuthenticationError < ApiError; end
|
|
33
|
+
class NotFoundError < ApiError; end
|
|
34
|
+
class RateLimitError < ApiError; end
|
|
35
|
+
end
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'faraday/retry'
|
|
5
|
+
require 'json'
|
|
6
|
+
require_relative '../sensitive_data'
|
|
7
|
+
|
|
8
|
+
module Dadata
|
|
9
|
+
# Faraday middleware that handles logging of requests and responses while ensuring
|
|
10
|
+
# sensitive data is properly sanitized. This middleware is automatically added to
|
|
11
|
+
# all DaData API clients.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class SensitiveDataMiddleware < Faraday::Middleware
|
|
15
|
+
include SensitiveData
|
|
16
|
+
|
|
17
|
+
# Processes the request, logs it securely, and handles any errors
|
|
18
|
+
#
|
|
19
|
+
# @param env [Hash] The request environment
|
|
20
|
+
# @return [Faraday::Response] The response from the next middleware
|
|
21
|
+
def call(env)
|
|
22
|
+
log_request(env)
|
|
23
|
+
@app.call(env).on_complete do |response_env|
|
|
24
|
+
log_response(response_env)
|
|
25
|
+
end
|
|
26
|
+
rescue Faraday::Error => e
|
|
27
|
+
log_error(e)
|
|
28
|
+
raise
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Logs the request details with sensitive data filtered
|
|
34
|
+
#
|
|
35
|
+
# @param env [Hash] The request environment
|
|
36
|
+
# @return [void]
|
|
37
|
+
def log_request(env)
|
|
38
|
+
return unless logger
|
|
39
|
+
|
|
40
|
+
logger.debug do
|
|
41
|
+
msg = "DaData Request: #{env.method.upcase} #{env.url}"
|
|
42
|
+
msg += "\nBody: #{request_body(env)}" if log_bodies? && request_body(env)
|
|
43
|
+
msg += "\nHeaders: #{sanitize_headers(env.request_headers)}"
|
|
44
|
+
msg
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the request payload (POST body or GET params) for logging, or nil
|
|
49
|
+
# when there is nothing to log. Only consulted when body logging is enabled.
|
|
50
|
+
#
|
|
51
|
+
# @param env [Hash] The request environment
|
|
52
|
+
# @return [String, nil]
|
|
53
|
+
def request_body(env)
|
|
54
|
+
body = env.body
|
|
55
|
+
return body if body && !body.empty?
|
|
56
|
+
|
|
57
|
+
params = env.params
|
|
58
|
+
params.inspect if params && !params.empty?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Whether request payloads may be logged. Off by default because payloads
|
|
62
|
+
# contain personal data this API processes.
|
|
63
|
+
#
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def log_bodies?
|
|
66
|
+
Dadata.configuration&.log_request_bodies || false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Logs the response details with sensitive data filtered
|
|
70
|
+
#
|
|
71
|
+
# @param env [Hash] The response environment
|
|
72
|
+
# @return [void]
|
|
73
|
+
def log_response(env)
|
|
74
|
+
return unless logger
|
|
75
|
+
|
|
76
|
+
logger.debug do
|
|
77
|
+
msg = "DaData Response: #{env.status}"
|
|
78
|
+
msg += "\nHeaders: #{sanitize_headers(env.response_headers)}"
|
|
79
|
+
msg
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Logs error details with sensitive data filtered
|
|
84
|
+
#
|
|
85
|
+
# @param error [Faraday::Error] The error that occurred
|
|
86
|
+
# @return [void]
|
|
87
|
+
def log_error(error)
|
|
88
|
+
return unless logger
|
|
89
|
+
|
|
90
|
+
logger.error do
|
|
91
|
+
msg = "DaData Error: #{error.class.name}"
|
|
92
|
+
msg += "\nMessage: #{sanitize_message(error.message)}"
|
|
93
|
+
msg += "\nHeaders: #{sanitize_headers(error.response[:response_headers] || {})}" if error.response
|
|
94
|
+
msg
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Gets the logger from the DaData configuration
|
|
99
|
+
#
|
|
100
|
+
# @return [Logger, nil] The configured logger
|
|
101
|
+
def logger
|
|
102
|
+
Dadata.configuration&.logger
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Base client class that handles HTTP communication with the DaData API.
|
|
107
|
+
# Implements secure logging and request/response handling.
|
|
108
|
+
#
|
|
109
|
+
# @api private
|
|
110
|
+
class ClientBase
|
|
111
|
+
include SensitiveData
|
|
112
|
+
|
|
113
|
+
# HTTP status codes and their descriptions
|
|
114
|
+
ERRORS = {
|
|
115
|
+
200 => 'Request processed successfully',
|
|
116
|
+
400 => 'Invalid request (invalid JSON or XML)',
|
|
117
|
+
401 => 'Missing API key or secret key, or non-existent key used',
|
|
118
|
+
403 => 'Invalid API key, unconfirmed email, or daily request limit exceeded',
|
|
119
|
+
404 => 'Service not found',
|
|
120
|
+
405 => 'Request method other than POST used',
|
|
121
|
+
413 => 'Request too long or too many conditions',
|
|
122
|
+
429 => 'Too many requests per second or new connections per minute',
|
|
123
|
+
500 => 'Internal service error'
|
|
124
|
+
}.freeze
|
|
125
|
+
|
|
126
|
+
# Maps HTTP status codes to the specific error class to raise.
|
|
127
|
+
# Any status not listed falls back to the generic ApiError.
|
|
128
|
+
STATUS_ERRORS = {
|
|
129
|
+
400 => BadRequestError,
|
|
130
|
+
401 => UnauthorizedError,
|
|
131
|
+
403 => AuthenticationError,
|
|
132
|
+
404 => NotFoundError,
|
|
133
|
+
429 => RateLimitError
|
|
134
|
+
}.freeze
|
|
135
|
+
|
|
136
|
+
# Creates a new client instance
|
|
137
|
+
#
|
|
138
|
+
# @param base_url [String] The base URL for API requests
|
|
139
|
+
# @param token [String] The API token for authentication
|
|
140
|
+
# @param secret [String, nil] Optional secret key for additional authentication
|
|
141
|
+
def initialize(base_url, token, secret = nil)
|
|
142
|
+
@base_url = base_url
|
|
143
|
+
@token = token
|
|
144
|
+
@secret = secret
|
|
145
|
+
@connection = build_connection
|
|
146
|
+
@logger = Dadata.configuration&.logger
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Submits a request to the API
|
|
150
|
+
#
|
|
151
|
+
# @param url [String] The endpoint URL
|
|
152
|
+
# @param data [Hash] The request data
|
|
153
|
+
# @param method [Symbol] The HTTP method to use (:get or :post)
|
|
154
|
+
# @param timeout [Integer] Request timeout in seconds
|
|
155
|
+
# @return [Hash] The parsed response
|
|
156
|
+
# @raise [ApiError] If the API returns an error
|
|
157
|
+
# @raise [ConnectionError] If there's a network error
|
|
158
|
+
def submit(url, data, method = :get, timeout: Dadata.timeout_sec)
|
|
159
|
+
response = send_request(url, data, method, timeout)
|
|
160
|
+
handle_response(response)
|
|
161
|
+
rescue Faraday::Error => e
|
|
162
|
+
handle_connection_error(e)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Closes the persistent connection, releasing any pooled sockets.
|
|
166
|
+
#
|
|
167
|
+
# @return [void]
|
|
168
|
+
def close
|
|
169
|
+
@connection.close if @connection.respond_to?(:close)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
# Builds the Faraday connection with appropriate middleware and settings
|
|
175
|
+
#
|
|
176
|
+
# @return [Faraday::Connection]
|
|
177
|
+
def build_connection
|
|
178
|
+
require 'faraday/net_http_persistent'
|
|
179
|
+
|
|
180
|
+
Faraday.new(@base_url) do |conn|
|
|
181
|
+
conn.request :json
|
|
182
|
+
conn.request :retry, {
|
|
183
|
+
max: 2,
|
|
184
|
+
interval: 0.05,
|
|
185
|
+
interval_randomness: 0.5,
|
|
186
|
+
backoff_factor: 2,
|
|
187
|
+
exceptions: [
|
|
188
|
+
Faraday::ConnectionFailed,
|
|
189
|
+
Faraday::TimeoutError,
|
|
190
|
+
'Timeout::Error'
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
conn.use SensitiveDataMiddleware
|
|
195
|
+
|
|
196
|
+
conn.response :json, content_type: /\bjson$/
|
|
197
|
+
|
|
198
|
+
conn.headers = {
|
|
199
|
+
'Content-Type' => 'application/json',
|
|
200
|
+
'Accept' => 'application/json',
|
|
201
|
+
'Authorization' => "Token #{@token}"
|
|
202
|
+
}
|
|
203
|
+
conn.headers['X-Secret'] = @secret if @secret
|
|
204
|
+
|
|
205
|
+
conn.adapter :net_http_persistent do |http|
|
|
206
|
+
http.read_timeout = Dadata.configuration&.timeout_sec || 10
|
|
207
|
+
http.open_timeout = Dadata.configuration&.timeout_sec || 10
|
|
208
|
+
http.write_timeout = Dadata.configuration&.timeout_sec || 10
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Sends a request to the API
|
|
214
|
+
#
|
|
215
|
+
# @param url [String] The endpoint URL
|
|
216
|
+
# @param data [Hash] The request data
|
|
217
|
+
# @param method [Symbol] The HTTP method to use (:get or :post)
|
|
218
|
+
# @param timeout [Integer] Request timeout in seconds
|
|
219
|
+
# @return [Faraday::Response] The response from the API
|
|
220
|
+
def send_request(url, data, method, timeout)
|
|
221
|
+
@connection.public_send(method) do |req|
|
|
222
|
+
req.url(url)
|
|
223
|
+
req.options.timeout = timeout
|
|
224
|
+
req.body = data.to_json unless method == :get
|
|
225
|
+
req.params = data if method == :get
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Handles the response from the API
|
|
230
|
+
#
|
|
231
|
+
# @param response [Faraday::Response] The response from the API
|
|
232
|
+
# @return [Hash] The parsed response
|
|
233
|
+
# @raise [ApiError] If the API returns an error
|
|
234
|
+
def handle_response(response)
|
|
235
|
+
return response.body if response.success?
|
|
236
|
+
|
|
237
|
+
error_message = ERRORS[response.status] || 'Unknown error'
|
|
238
|
+
log_error("API Error: #{error_message} (#{response.status})")
|
|
239
|
+
|
|
240
|
+
error_class = STATUS_ERRORS.fetch(response.status, ApiError)
|
|
241
|
+
raise error_class.new(response.status, error_message)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Handles connection errors
|
|
245
|
+
#
|
|
246
|
+
# @param error [Faraday::Error] The error that occurred
|
|
247
|
+
# @raise [ConnectionError] If there's a network error
|
|
248
|
+
def handle_connection_error(error)
|
|
249
|
+
sanitized_error = sanitize_message(error.message)
|
|
250
|
+
log_error("Connection Error: #{sanitized_error}")
|
|
251
|
+
log_error("Headers: #{sanitize_headers(@connection.headers)}")
|
|
252
|
+
|
|
253
|
+
case error
|
|
254
|
+
when Faraday::TimeoutError
|
|
255
|
+
raise TimeoutError, 'Request timed out'
|
|
256
|
+
when Faraday::ConnectionFailed
|
|
257
|
+
raise ConnectionError, 'Failed to connect'
|
|
258
|
+
else
|
|
259
|
+
raise ConnectionError, 'Request failed'
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Logs an error, with sensitive data filtered. Sanitization comes from
|
|
264
|
+
# the shared SensitiveData module.
|
|
265
|
+
#
|
|
266
|
+
# @param message [String] The error message
|
|
267
|
+
# @return [void]
|
|
268
|
+
def log_error(message)
|
|
269
|
+
@logger&.error(sanitize_message(message))
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Dadata
|
|
6
|
+
# Client for data cleaning and standardization operations
|
|
7
|
+
class CleanClient < ClientBase
|
|
8
|
+
BASE_URL = 'https://cleaner.dadata.ru/api/v1/'
|
|
9
|
+
|
|
10
|
+
def initialize(token = Dadata.api_key, secret = Dadata.secret_key)
|
|
11
|
+
super(BASE_URL, token, secret)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Clean and standardize a single value
|
|
15
|
+
#
|
|
16
|
+
# @param name [String] Type of cleaning to apply:
|
|
17
|
+
# - address - postal address
|
|
18
|
+
# - phone - phone number
|
|
19
|
+
# - passport - passport number
|
|
20
|
+
# - name - full name
|
|
21
|
+
# - email - email address
|
|
22
|
+
# - birthdate - date
|
|
23
|
+
# - vehicle - vehicle brand and model
|
|
24
|
+
# - simple_party_name - company name
|
|
25
|
+
# @param source [String] Value to clean
|
|
26
|
+
# @return [Hash, nil] Cleaned data or nil if cleaning failed
|
|
27
|
+
def clean(name, source)
|
|
28
|
+
response = submit("clean/#{name}", [source], :post)
|
|
29
|
+
response&.first
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Clean and standardize a composite record
|
|
33
|
+
#
|
|
34
|
+
# @param structure [Array<String>] Record structure with fields:
|
|
35
|
+
# - AS_IS - leave as is (no standardization)
|
|
36
|
+
# - SIMPLE_PARTY_NAME - parse company name
|
|
37
|
+
# - NAME - parse as full name
|
|
38
|
+
# - BIRTHDATE - parse as date
|
|
39
|
+
# - ADDRESS - parse as address
|
|
40
|
+
# - PHONE - parse as phone
|
|
41
|
+
# - PASSPORT - passport number
|
|
42
|
+
# - EMAIL - email address
|
|
43
|
+
# - VEHICLE - vehicle brand and model
|
|
44
|
+
# @param record [Array<String>] Record to process; field order must match structure
|
|
45
|
+
# @return [Hash, nil] Cleaned data or nil if cleaning failed
|
|
46
|
+
def clean_record(structure, record)
|
|
47
|
+
data = { structure:, data: [record] }
|
|
48
|
+
response = submit('clean', data, :post)
|
|
49
|
+
response&.dig('data', 0)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require 'date'
|
|
5
|
+
|
|
6
|
+
module Dadata
|
|
7
|
+
# Client for managing DaData subscriber profile
|
|
8
|
+
class ProfileClient < ClientBase
|
|
9
|
+
BASE_URL = 'https://dadata.ru/api/v2/'
|
|
10
|
+
|
|
11
|
+
def initialize(token = Dadata.api_key, secret = Dadata.secret_key)
|
|
12
|
+
super(BASE_URL, token, secret)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Get current balance
|
|
16
|
+
#
|
|
17
|
+
# @return [Numeric, nil] Current balance or nil if request failed
|
|
18
|
+
def balance
|
|
19
|
+
response = submit('profile/balance', {}, :get)
|
|
20
|
+
response&.fetch('balance', nil)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get daily statistics
|
|
24
|
+
#
|
|
25
|
+
# @param date [String, nil] Date to get statistics for (ISO 8601 format)
|
|
26
|
+
# @return [Hash, nil] Daily statistics or nil if request failed
|
|
27
|
+
def daily_stats(date = nil)
|
|
28
|
+
date = date.nil? ? Date.today : handle_date(date)
|
|
29
|
+
submit('stat/daily', { date: date.iso8601 }, :get)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get API versions
|
|
33
|
+
#
|
|
34
|
+
# @return [Hash, nil] Version information or nil if request failed
|
|
35
|
+
def versions
|
|
36
|
+
submit('version', {}, :get)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Convert string to Date object
|
|
42
|
+
#
|
|
43
|
+
# @param date_string [String] Date string in any parseable format
|
|
44
|
+
# @return [Date] Parsed date or today's date if parsing fails
|
|
45
|
+
def handle_date(date_string)
|
|
46
|
+
Date.parse(date_string.to_s)
|
|
47
|
+
rescue ArgumentError, TypeError
|
|
48
|
+
Date.today
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Dadata
|
|
6
|
+
# Client for data suggestions and lookups
|
|
7
|
+
class SuggestClient < ClientBase
|
|
8
|
+
BASE_URL = 'https://suggestions.dadata.ru/suggestions/api/4_1/rs/'
|
|
9
|
+
|
|
10
|
+
def initialize(token = Dadata.api_key, secret = Dadata.secret_key)
|
|
11
|
+
super(BASE_URL, token, secret)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Find addresses by geographic coordinates
|
|
15
|
+
#
|
|
16
|
+
# @param name [String] Type of entity to search for (e.g., 'address', 'postal_unit')
|
|
17
|
+
# @param lat [Numeric] Latitude
|
|
18
|
+
# @param lon [Numeric] Longitude
|
|
19
|
+
# @param radius_meters [Integer] Search radius in meters (default: 100, max: 1000)
|
|
20
|
+
# @param kwargs [Hash] Additional parameters (e.g., language, count)
|
|
21
|
+
# @return [Array<Hash>, nil] List of suggestions or nil if not found
|
|
22
|
+
def geolocate(name, lat, lon, radius_meters = 100, **kwargs)
|
|
23
|
+
data = { lat:, lon:, radius_meters: }.merge(kwargs)
|
|
24
|
+
response = submit("geolocate/#{name}", data, :post)
|
|
25
|
+
response&.fetch('suggestions', nil)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get address by IP
|
|
29
|
+
#
|
|
30
|
+
# @param query [String] IP address
|
|
31
|
+
# @param kwargs [Hash] Additional parameters (e.g., language)
|
|
32
|
+
# @return [Hash, nil] Location data or nil if not found
|
|
33
|
+
def iplocate(query, **kwargs)
|
|
34
|
+
data = { ip: query }.merge(kwargs)
|
|
35
|
+
response = submit('iplocate/address', data, :get)
|
|
36
|
+
response&.fetch('location', nil)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get suggestions for partial input
|
|
40
|
+
#
|
|
41
|
+
# @param name [String] Type of entity to search for
|
|
42
|
+
# @param query [String] Search query
|
|
43
|
+
# @param count [Integer] Maximum number of results
|
|
44
|
+
# @param kwargs [Hash] Additional parameters (e.g., language, constraints)
|
|
45
|
+
# @return [Array<Hash>, nil] List of suggestions or nil if not found
|
|
46
|
+
def suggest(name, query, count = Dadata.suggestions_count, **kwargs)
|
|
47
|
+
data = { query:, count: }.merge(kwargs)
|
|
48
|
+
response = submit("suggest/#{name}", data, :post)
|
|
49
|
+
response&.fetch('suggestions', nil)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Find entities by identifier
|
|
53
|
+
#
|
|
54
|
+
# @param name [String] Type of entity to search for
|
|
55
|
+
# @param query [String] Entity identifier
|
|
56
|
+
# @param count [Integer] Maximum number of results
|
|
57
|
+
# @param kwargs [Hash] Additional parameters (e.g., language)
|
|
58
|
+
# @return [Array<Hash>, nil] List of suggestions or nil if not found
|
|
59
|
+
def find_by_id(name, query, count = Dadata.suggestions_count, **kwargs)
|
|
60
|
+
data = { query:, count: }.merge(kwargs)
|
|
61
|
+
response = submit("findById/#{name}", data, :post)
|
|
62
|
+
response&.fetch('suggestions', nil)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Find companies by email address
|
|
66
|
+
#
|
|
67
|
+
# @param query [String] Email address
|
|
68
|
+
# @return [Array<Hash>, nil] List of companies or nil if not found
|
|
69
|
+
def find_by_email(query)
|
|
70
|
+
response = submit('findByEmail/company', { query: }, :post)
|
|
71
|
+
response&.fetch('suggestions', nil)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Find affiliated companies
|
|
75
|
+
#
|
|
76
|
+
# @param query [String] Company identifier
|
|
77
|
+
# @param count [Integer] Maximum number of results
|
|
78
|
+
# @param kwargs [Hash] Additional parameters (e.g., scope, type)
|
|
79
|
+
# @return [Array<Hash>, nil] List of affiliated companies or nil if not found
|
|
80
|
+
def find_affiliated(query, count = Dadata.suggestions_count, **kwargs)
|
|
81
|
+
data = { query:, count: }.merge(kwargs)
|
|
82
|
+
response = submit('findAffiliated/party', data, :post)
|
|
83
|
+
response&.fetch('suggestions', nil)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dadata
|
|
4
|
+
# The SensitiveData module provides functionality for sanitizing sensitive information
|
|
5
|
+
# in log messages and HTTP headers. It is used internally by the gem to ensure that
|
|
6
|
+
# sensitive data such as API keys and secrets are not exposed in logs.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# class MyLogger
|
|
10
|
+
# include SensitiveData
|
|
11
|
+
#
|
|
12
|
+
# def log_request(headers)
|
|
13
|
+
# puts sanitize_headers(headers)
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
module SensitiveData
|
|
17
|
+
# List of headers that contain sensitive information and should be filtered
|
|
18
|
+
SENSITIVE_HEADERS = %w[Authorization X-Secret API-Key].freeze
|
|
19
|
+
|
|
20
|
+
# Sanitizes headers by replacing sensitive values with [FILTERED]
|
|
21
|
+
#
|
|
22
|
+
# @param headers [Hash, nil] Headers to sanitize
|
|
23
|
+
# @return [String] Sanitized headers string
|
|
24
|
+
# @example
|
|
25
|
+
# headers = { 'API-Key' => 'secret', 'Content-Type' => 'application/json' }
|
|
26
|
+
# sanitize_headers(headers) # => "API-Key: [FILTERED], Content-Type: application/json"
|
|
27
|
+
def sanitize_headers(headers)
|
|
28
|
+
return '' unless headers
|
|
29
|
+
|
|
30
|
+
headers.map do |key, value|
|
|
31
|
+
if SENSITIVE_HEADERS.include?(key)
|
|
32
|
+
"#{key}: [FILTERED]"
|
|
33
|
+
else
|
|
34
|
+
"#{key}: #{value}"
|
|
35
|
+
end
|
|
36
|
+
end.join(', ')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Sanitizes a message by replacing sensitive information with [FILTERED]
|
|
40
|
+
#
|
|
41
|
+
# @param msg [String, nil] Message to sanitize
|
|
42
|
+
# @return [String] Sanitized message
|
|
43
|
+
# @example
|
|
44
|
+
# msg = "API-Key: secret123, Content-Type: application/json"
|
|
45
|
+
# sanitize_message(msg) # => "API-Key: [FILTERED], Content-Type: application/json"
|
|
46
|
+
def sanitize_message(msg)
|
|
47
|
+
return '' unless msg.is_a?(String)
|
|
48
|
+
|
|
49
|
+
result = msg.dup
|
|
50
|
+
SENSITIVE_HEADERS.each do |header|
|
|
51
|
+
# Escape hyphens in the header name and handle any whitespace around the colon
|
|
52
|
+
pattern = /#{Regexp.escape(header)}[\s]*:[\s]*[^\n,]+/
|
|
53
|
+
result = result.gsub(pattern, "#{header}: [FILTERED]")
|
|
54
|
+
end
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/dadata-rb.rb
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Lets `require 'dadata-rb'` (the published gem name) load the library, whose
|
|
4
|
+
# entry point and `Dadata` module live in `dadata.rb`. This keeps Bundler's
|
|
5
|
+
# default auto-require working for consumers who add `gem 'dadata-rb'`.
|
|
6
|
+
require_relative 'dadata'
|