weaviate-ruby 0.9.2 → 0.10.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.
@@ -4,17 +4,29 @@ module Weaviate
4
4
  class Backups < Base
5
5
  PATH = "backups"
6
6
 
7
+ # List backups on a backend.
8
+ # @param order [String] "asc" or "desc" (default) by start time
9
+ def list(backend:, order: nil)
10
+ response = client.connection.get("#{PATH}/#{backend}") do |req|
11
+ req.params["order"] = order unless order.nil?
12
+ end
13
+ response.body
14
+ end
15
+
16
+ # @param config [Hash] Backup configuration, e.g. { Bucket: "my-bucket", Path: "backups", CPUPercentage: 80 }
7
17
  def create(
8
18
  backend:,
9
19
  id:,
10
20
  include: nil,
11
- exclude: nil
21
+ exclude: nil,
22
+ config: nil
12
23
  )
13
24
  response = client.connection.post("#{PATH}/#{backend}") do |req|
14
25
  req.body = {}
15
26
  req.body["id"] = id
16
27
  req.body["include"] = include if include
17
28
  req.body["exclude"] = exclude if exclude
29
+ req.body["config"] = config if config
18
30
  end
19
31
 
20
32
  response.body
@@ -22,22 +34,41 @@ module Weaviate
22
34
 
23
35
  def get(
24
36
  backend:,
25
- id:
37
+ id:,
38
+ bucket: nil,
39
+ path: nil
26
40
  )
27
- response = client.connection.get("#{PATH}/#{backend}/#{id}")
41
+ response = client.connection.get("#{PATH}/#{backend}/#{id}") do |req|
42
+ location_params(req, bucket, path)
43
+ end
28
44
  response.body
29
45
  end
30
46
 
47
+ # Cancel a backup that is in progress.
48
+ def cancel(
49
+ backend:,
50
+ id:,
51
+ bucket: nil,
52
+ path: nil
53
+ )
54
+ response = client.connection.delete("#{PATH}/#{backend}/#{id}") do |req|
55
+ location_params(req, bucket, path)
56
+ end
57
+ response.success?
58
+ end
59
+
31
60
  def restore(
32
61
  backend:,
33
62
  id:,
34
63
  include: nil,
35
- exclude: nil
64
+ exclude: nil,
65
+ config: nil
36
66
  )
37
67
  response = client.connection.post("#{PATH}/#{backend}/#{id}/restore") do |req|
38
68
  req.body = {}
39
69
  req.body["include"] = include if include
40
70
  req.body["exclude"] = exclude if exclude
71
+ req.body["config"] = config if config
41
72
  end
42
73
 
43
74
  response.body
@@ -45,10 +76,34 @@ module Weaviate
45
76
 
46
77
  def restore_status(
47
78
  backend:,
48
- id:
79
+ id:,
80
+ bucket: nil,
81
+ path: nil
49
82
  )
50
- response = client.connection.get("#{PATH}/#{backend}/#{id}/restore")
83
+ response = client.connection.get("#{PATH}/#{backend}/#{id}/restore") do |req|
84
+ location_params(req, bucket, path)
85
+ end
51
86
  response.body
52
87
  end
88
+
89
+ # Cancel a restore that is in progress.
90
+ def cancel_restore(
91
+ backend:,
92
+ id:,
93
+ bucket: nil,
94
+ path: nil
95
+ )
96
+ response = client.connection.delete("#{PATH}/#{backend}/#{id}/restore") do |req|
97
+ location_params(req, bucket, path)
98
+ end
99
+ response.success?
100
+ end
101
+
102
+ private
103
+
104
+ def location_params(req, bucket, path)
105
+ req.params["bucket"] = bucket unless bucket.nil?
106
+ req.params["path"] = path unless path.nil?
107
+ end
53
108
  end
54
109
  end
@@ -27,9 +27,7 @@ module Weaviate
27
27
  req.body["filters"] = filters if filters
28
28
  end
29
29
 
30
- if response.success?
31
- response.body
32
- end
30
+ response.body
33
31
  end
34
32
  end
35
33
  end
@@ -1,47 +1,91 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "faraday"
4
- require "graphlient"
3
+ require "logger"
5
4
 
6
5
  module Weaviate
7
6
  class Client
8
- attr_reader :url, :api_key, :model_service, :model_service_api_key, :adapter, :logger
7
+ attr_reader :url, :api_key, :model_service, :model_service_api_key, :headers, :proxy, :logger,
8
+ :open_timeout, :read_timeout, :write_timeout
9
9
 
10
10
  API_VERSION = "v1"
11
11
 
12
+ # Headers Weaviate reads model provider API keys from. Providers that need more than one
13
+ # header (e.g. AWS) or a custom base URL can be configured with the `headers:` option.
12
14
  API_KEY_HEADERS = {
13
- ollama: "X-Ollama-Not-Used",
14
- openai: "X-OpenAI-Api-Key",
15
+ anthropic: "X-Anthropic-Api-Key",
16
+ anyscale: "X-Anyscale-Api-Key",
15
17
  azure_openai: "X-Azure-Api-Key",
16
18
  cohere: "X-Cohere-Api-Key",
19
+ contextualai: "X-ContextualAI-Api-Key",
20
+ databricks: "X-Databricks-Token",
21
+ deepseek: "X-Deepseek-Api-Key",
22
+ digitalocean: "X-Digitalocean-Api-Key",
23
+ friendliai: "X-Friendli-Token",
24
+ google: "X-Goog-Api-Key",
25
+ google_palm: "X-Palm-Api-Key",
26
+ google_studio: "X-Goog-Studio-Api-Key",
27
+ google_vertex: "X-Goog-Vertex-Api-Key",
17
28
  huggingface: "X-HuggingFace-Api-Key",
18
- google_palm: "X-Palm-Api-Key"
29
+ jinaai: "X-JinaAI-Api-Key",
30
+ mistral: "X-Mistral-Api-Key",
31
+ nvidia: "X-Nvidia-Api-Key",
32
+ ollama: "X-Ollama-Not-Used",
33
+ openai: "X-OpenAI-Api-Key",
34
+ twelvelabs: "X-Twelvelabs-Api-Key",
35
+ voyageai: "X-VoyageAI-Api-Key",
36
+ xai: "X-Xai-Api-Key"
19
37
  }
20
38
 
39
+ # @param url [String] The Weaviate instance URL
40
+ # @param api_key [String] The Weaviate API key
41
+ # @param model_service [Symbol] The service used to generate vectors, one of the API_KEY_HEADERS keys
42
+ # @param model_service_api_key [String] The API key for the model service
43
+ # @param headers [Hash] Additional headers sent with every request, e.g. { "X-Aws-Access-Key" => "...", "X-Aws-Secret-Key" => "..." }
44
+ # @param proxy [String] Proxy URL, e.g. "http://user:pass@localhost:8888". Defaults to the http_proxy/https_proxy env vars.
45
+ # @param logger [Logger] Logger for requests and responses
46
+ # @param open_timeout [Numeric] Seconds to wait for a connection to open. Defaults to Net::HTTP's 60 seconds.
47
+ # @param read_timeout [Numeric] Seconds to wait for a response. Defaults to Net::HTTP's 60 seconds.
48
+ # @param write_timeout [Numeric] Seconds to wait for a request to be written. Defaults to Net::HTTP's 60 seconds.
49
+ # @param adapter [Object] Deprecated and ignored, Faraday is no longer used
21
50
  def initialize(
22
51
  url:,
23
52
  api_key: nil,
24
53
  model_service: nil,
25
54
  model_service_api_key: nil,
26
- adapter: Faraday.default_adapter,
27
- logger: nil
55
+ headers: {},
56
+ proxy: nil,
57
+ logger: nil,
58
+ open_timeout: nil,
59
+ read_timeout: nil,
60
+ write_timeout: nil,
61
+ adapter: nil
28
62
  )
29
63
  validate_model_service!(model_service) unless model_service.nil?
64
+ warn "[weaviate-ruby] The `adapter:` option is deprecated and ignored, Faraday is no longer used." unless adapter.nil?
30
65
 
31
66
  @url = url
32
67
  @api_key = api_key
33
68
  @model_service = model_service
34
69
  @model_service_api_key = model_service_api_key
35
- @adapter = adapter
70
+ @headers = headers
71
+ @proxy = proxy
36
72
  @logger = logger || Logger.new($stdout)
73
+ @open_timeout = open_timeout
74
+ @read_timeout = read_timeout
75
+ @write_timeout = write_timeout
37
76
  end
38
77
 
39
78
  def oidc
40
79
  Weaviate::OIDC.new(client: self).get
41
80
  end
42
81
 
82
+ def collections
83
+ @collections ||= Weaviate::Collections.new(client: self)
84
+ end
85
+
86
+ # @deprecated Weaviate now refers to schema classes as collections. Use {#collections} instead.
43
87
  def schema
44
- @schema ||= Weaviate::Schema.new(client: self)
88
+ collections
45
89
  end
46
90
 
47
91
  def meta
@@ -49,9 +93,10 @@ module Weaviate
49
93
  @meta.get
50
94
  end
51
95
 
52
- def nodes
96
+ # @see Weaviate::Nodes#list
97
+ def nodes(**options)
53
98
  @nodes ||= Weaviate::Nodes.new(client: self)
54
- @nodes.list
99
+ @nodes.list(**options)
55
100
  end
56
101
 
57
102
  def live?
@@ -80,44 +125,47 @@ module Weaviate
80
125
  @query ||= Weaviate::Query.new(client: self)
81
126
  end
82
127
 
83
- def graphql
84
- headers = {}
128
+ def aliases
129
+ @aliases ||= Weaviate::Aliases.new(client: self)
130
+ end
85
131
 
86
- if model_service && model_service_api_key
87
- headers[API_KEY_HEADERS[model_service]] = model_service_api_key
88
- end
132
+ def users
133
+ @users ||= Weaviate::Users.new(client: self)
134
+ end
89
135
 
90
- if api_key
91
- headers["Authorization"] = "Bearer #{api_key}"
92
- end
136
+ def roles
137
+ @roles ||= Weaviate::Roles.new(client: self)
138
+ end
93
139
 
94
- @graphql ||= Graphlient::Client.new(
95
- "#{url}/#{API_VERSION}/graphql",
96
- headers: headers,
97
- http_options: {
98
- read_timeout: 20,
99
- write_timeout: 30
100
- }
101
- )
140
+ def groups
141
+ @groups ||= Weaviate::Groups.new(client: self)
142
+ end
143
+
144
+ def graphql
145
+ @graphql ||= Weaviate::GraphQL.new(client: self)
102
146
  end
103
147
 
104
148
  def connection
105
- @connection ||= Faraday.new(url: "#{url}/#{API_VERSION}/") do |faraday|
106
- if api_key
107
- faraday.request :authorization, :Bearer, api_key
108
- end
109
- faraday.request :json
110
- faraday.response :logger, logger, {headers: true, bodies: true, errors: true}
111
- faraday.response :json, content_type: /\bjson$/
112
- faraday.response :raise_error
113
- faraday.adapter adapter
114
-
115
- faraday.headers[API_KEY_HEADERS[model_service]] = model_service_api_key if model_service && model_service_api_key
116
- end
149
+ @connection ||= Weaviate::Connection.new(
150
+ url: "#{url}/#{API_VERSION}",
151
+ headers: default_headers.merge(headers),
152
+ logger: logger,
153
+ proxy: proxy,
154
+ open_timeout: open_timeout,
155
+ read_timeout: read_timeout,
156
+ write_timeout: write_timeout
157
+ )
117
158
  end
118
159
 
119
160
  private
120
161
 
162
+ def default_headers
163
+ headers = {"User-Agent" => "weaviate-ruby/#{Weaviate::VERSION}"}
164
+ headers["Authorization"] = "Bearer #{api_key}" if api_key
165
+ headers[API_KEY_HEADERS[model_service]] = model_service_api_key if model_service && model_service_api_key
166
+ headers
167
+ end
168
+
121
169
  def validate_model_service!(model_service)
122
170
  unless API_KEY_HEADERS.key?(model_service)
123
171
  raise ArgumentError, "Invalid model service: #{model_service}. Acceptable values are: #{API_KEY_HEADERS.keys.join(", ")}"
@@ -1,27 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module Weaviate
4
- class Schema < Base
6
+ # Manage collections (formerly known as "classes" in the Weaviate schema).
7
+ class Collections < Base
5
8
  PATH = "schema"
6
9
 
7
- # Dumps the current Weaviate schema. The result contains an array of objects.
10
+ # Lists all collections in the Weaviate schema.
8
11
  def list
9
12
  response = client.connection.get(PATH)
10
13
  response.body
11
14
  end
12
15
 
13
- # Get a single class from the schema
16
+ # Get a single collection
14
17
  def get(class_name:)
15
18
  response = client.connection.get("#{PATH}/#{class_name}")
16
-
17
- if response.success?
18
- response.body
19
- elsif response.status == 404
20
- response.reason_phrase
21
- end
19
+ response.body
22
20
  end
23
21
 
24
- # Create a new data object class in the schema.
22
+ # Create a new collection.
25
23
  def create(
26
24
  class_name:,
27
25
  description: nil,
@@ -34,7 +32,8 @@ module Weaviate
34
32
  vectorizer: nil,
35
33
  module_config: nil,
36
34
  inverted_index_config: nil,
37
- replication_config: nil
35
+ replication_config: nil,
36
+ vector_config: nil
38
37
  )
39
38
  response = client.connection.post(PATH) do |req|
40
39
  req.body = {}
@@ -56,26 +55,21 @@ module Weaviate
56
55
 
57
56
  req.body["invertedIndexConfig"] = inverted_index_config unless inverted_index_config.nil?
58
57
  req.body["replicationConfig"] = replication_config unless replication_config.nil?
58
+ req.body["vectorConfig"] = vector_config unless vector_config.nil?
59
59
  end
60
60
 
61
61
  response.body
62
62
  end
63
63
 
64
- # Remove a class (and all data in the instances) from the schema.
64
+ # Remove a collection (and all data in the instances).
65
65
  def delete(class_name:)
66
66
  response = client.connection.delete("#{PATH}/#{class_name}")
67
-
68
- if response.success?
69
- response.body.empty?
70
- else
71
- response.body
72
- end
67
+ response.body.empty?
73
68
  end
74
69
 
75
- # Update settings of an existing schema class.
76
- # TODO: Fix it.
77
- # This endpoint keeps returning the following error:
78
- # => {"error"=>[{"message"=>"properties cannot be updated through updating the class. Use the add property feature (e.g. \"POST /v1/schema/{className}/properties\") to add additional properties"}]}
70
+ # Update settings of an existing collection.
71
+ # Weaviate replaces the whole collection definition on update, so the provided settings are merged
72
+ # into the current definition. Immutable settings and existing properties cannot be changed.
79
73
  def update(
80
74
  class_name:,
81
75
  description: nil,
@@ -85,45 +79,73 @@ module Weaviate
85
79
  module_config: nil,
86
80
  properties: nil,
87
81
  inverted_index_config: nil,
88
- replication_config: nil
82
+ replication_config: nil,
83
+ vector_config: nil
89
84
  )
85
+ changes = {
86
+ "description" => description,
87
+ "vectorIndexType" => vector_index_type,
88
+ "vectorIndexConfig" => vector_index_config,
89
+ "vectorizer" => vectorizer,
90
+ "moduleConfig" => module_config,
91
+ "properties" => properties,
92
+ "invertedIndexConfig" => inverted_index_config,
93
+ "replicationConfig" => replication_config,
94
+ "vectorConfig" => vector_config
95
+ }.compact
96
+
90
97
  response = client.connection.put("#{PATH}/#{class_name}") do |req|
91
- req.body = {}
92
- req.body["class"] = class_name unless class_name.nil?
93
- req.body["description"] = description unless description.nil?
94
- req.body["vectorIndexType"] = vector_index_type unless vector_index_type.nil?
95
- req.body["vectorIndexConfig"] = vector_index_config unless vector_index_config.nil?
96
- req.body["vectorizer"] = vectorizer unless vectorizer.nil?
97
- req.body["moduleConfig"] = module_config unless module_config.nil?
98
- req.body["properties"] = properties unless properties.nil?
99
- req.body["invertedIndexConfig"] = inverted_index_config unless inverted_index_config.nil?
100
- req.body["replicationConfig"] = replication_config unless replication_config.nil?
98
+ req.body = deep_merge(get(class_name: class_name), JSON.parse(changes.to_json))
101
99
  end
102
100
 
103
- if response.success?
104
- end
105
101
  response.body
106
102
  end
107
103
 
108
- # Adds one or more tenants to a class.
104
+ # Adds one or more tenants to a collection.
105
+ # @param tenants [Array<String, Hash>] Tenant names, or hashes such as { name: "a", activityStatus: "COLD" }
109
106
  def add_tenants(
110
107
  class_name:,
111
108
  tenants:
112
109
  )
113
110
  response = client.connection.post("#{PATH}/#{class_name}/tenants") do |req|
114
- tenants_str = tenants.map { |t| %({"name": "#{t}"}) }.join(", ")
115
- req.body = "[#{tenants_str}]"
111
+ req.body = tenants_body(tenants)
112
+ end
113
+ response.body
114
+ end
115
+
116
+ # Update the activity status of one or more tenants.
117
+ # @param tenants [Array<Hash>] e.g. [{ name: "a", activityStatus: "COLD" }]
118
+ def update_tenants(
119
+ class_name:,
120
+ tenants:
121
+ )
122
+ response = client.connection.put("#{PATH}/#{class_name}/tenants") do |req|
123
+ req.body = tenants_body(tenants)
116
124
  end
117
125
  response.body
118
126
  end
119
127
 
120
- # List tenants of a class.
128
+ # Get a single tenant of a collection.
129
+ def get_tenant(class_name:, tenant:)
130
+ response = client.connection.get("#{PATH}/#{class_name}/tenants/#{tenant}")
131
+ response.body
132
+ end
133
+
134
+ # Check if a tenant exists.
135
+ def tenant_exists?(class_name:, tenant:)
136
+ response = client.connection.head("#{PATH}/#{class_name}/tenants/#{tenant}")
137
+ response.success?
138
+ rescue Weaviate::ResourceNotFound
139
+ false
140
+ end
141
+
142
+ # List tenants of a collection.
121
143
  def list_tenants(class_name:)
122
144
  response = client.connection.get("#{PATH}/#{class_name}/tenants")
123
145
  response.body
124
146
  end
125
147
 
126
- # Remove one or more tenants from a class.
148
+ # Remove one or more tenants from a collection.
127
149
  def remove_tenants(
128
150
  class_name:,
129
151
  tenants:
@@ -132,13 +154,10 @@ module Weaviate
132
154
  req.body = tenants
133
155
  end
134
156
 
135
- if response.success?
136
- end
137
-
138
157
  response.body
139
158
  end
140
159
 
141
- # Add a property to an existing schema class.
160
+ # Add a property to an existing collection.
142
161
  def add_property(
143
162
  class_name:,
144
163
  property:
@@ -147,15 +166,15 @@ module Weaviate
147
166
  req.body = property
148
167
  end
149
168
 
150
- if response.success?
151
- end
152
169
  response.body
153
170
  end
154
171
 
155
- # Inspect the shards of a class
156
- def shards(class_name:)
157
- response = client.connection.get("#{PATH}/#{class_name}/shards")
158
- response.body if response.success?
172
+ # Inspect the shards of a collection
173
+ def shards(class_name:, tenant: nil)
174
+ response = client.connection.get("#{PATH}/#{class_name}/shards") do |req|
175
+ req.params["tenant"] = tenant unless tenant.nil?
176
+ end
177
+ response.body
159
178
  end
160
179
 
161
180
  # Update shard status
@@ -166,15 +185,29 @@ module Weaviate
166
185
  req.body = {}
167
186
  req.body["status"] = status
168
187
  end
169
- response.body if response.success?
188
+
189
+ response.body
170
190
  end
171
191
 
172
192
  private
173
193
 
194
+ def tenants_body(tenants)
195
+ Array(tenants).map { |tenant| tenant.is_a?(Hash) ? tenant : {name: tenant.to_s} }
196
+ end
197
+
198
+ def deep_merge(current, changes)
199
+ current.merge(changes) do |_key, current_value, new_value|
200
+ (current_value.is_a?(Hash) && new_value.is_a?(Hash)) ? deep_merge(current_value, new_value) : new_value
201
+ end
202
+ end
203
+
174
204
  def validate_status!(status)
175
205
  unless %w[READONLY READY].include?(status.to_s.upcase)
176
206
  raise ArgumentError, 'status must be either "READONLY" or "READY"'
177
207
  end
178
208
  end
179
209
  end
210
+
211
+ # @deprecated Weaviate now refers to schema classes as collections. Use {Collections} instead.
212
+ Schema = Collections
180
213
  end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Weaviate
8
+ # A minimal HTTP client built on Net::HTTP
9
+ class Connection
10
+ Request = Struct.new(:params, :headers, :body)
11
+
12
+ Response = Struct.new(:status, :reason_phrase, :headers, :body, keyword_init: true) do
13
+ def success?
14
+ (200..299).cover?(status)
15
+ end
16
+ end
17
+
18
+ METHODS = {
19
+ get: Net::HTTP::Get,
20
+ head: Net::HTTP::Head,
21
+ post: Net::HTTP::Post,
22
+ put: Net::HTTP::Put,
23
+ patch: Net::HTTP::Patch,
24
+ delete: Net::HTTP::Delete
25
+ }
26
+
27
+ ERRORS = {
28
+ 400 => BadRequestError,
29
+ 401 => UnauthorizedError,
30
+ 403 => ForbiddenError,
31
+ 404 => ResourceNotFound,
32
+ 409 => ConflictError,
33
+ 422 => UnprocessableEntityError
34
+ }
35
+
36
+ FILTERED_HEADERS = /authorization|api-key|token|secret|access-key/i
37
+
38
+ attr_reader :url, :headers, :logger, :proxy, :timeouts
39
+
40
+ def initialize(url:, headers: {}, logger: nil, proxy: nil, open_timeout: nil, read_timeout: nil, write_timeout: nil)
41
+ @url = url.chomp("/")
42
+ @headers = headers
43
+ @logger = logger
44
+ @proxy = proxy && URI(proxy)
45
+ @timeouts = {open_timeout: open_timeout, read_timeout: read_timeout, write_timeout: write_timeout}.compact
46
+ end
47
+
48
+ METHODS.each_key do |method|
49
+ define_method(method) do |path, &block|
50
+ request(method, path, &block)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def request(method, path)
57
+ req = Request.new({}, {}, nil)
58
+ yield req if block_given?
59
+
60
+ uri = URI("#{url}/#{path}")
61
+ uri.query = URI.encode_www_form(req.params) unless req.params.nil? || req.params.empty?
62
+
63
+ http_request = METHODS.fetch(method).new(uri)
64
+ headers.merge(req.headers).each { |k, v| http_request[k] = v }
65
+
66
+ unless req.body.nil?
67
+ http_request["Content-Type"] = "application/json"
68
+ http_request.body = req.body.is_a?(String) ? req.body : req.body.to_json
69
+ end
70
+
71
+ log_request(http_request, uri)
72
+
73
+ http_response = perform(uri, http_request)
74
+ response = build_response(http_response)
75
+
76
+ log_response(response, http_response)
77
+
78
+ raise_error!(response) unless response.success?
79
+
80
+ response
81
+ end
82
+
83
+ def perform(uri, http_request)
84
+ Net::HTTP.start(
85
+ uri.host,
86
+ uri.port,
87
+ proxy ? proxy.host : :ENV,
88
+ proxy&.port,
89
+ proxy&.user,
90
+ proxy&.password,
91
+ use_ssl: uri.scheme == "https",
92
+ **timeouts
93
+ ) do |http|
94
+ http.request(http_request)
95
+ end
96
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout => e
97
+ raise Weaviate::TimeoutError, e.message
98
+ rescue SocketError, SystemCallError, IOError, OpenSSL::SSL::SSLError => e
99
+ raise Weaviate::ConnectionFailed, e.message
100
+ end
101
+
102
+ def build_response(http_response)
103
+ body = http_response.body.to_s
104
+
105
+ if http_response["Content-Type"].to_s.match?(/\bjson\b/) && !body.empty?
106
+ body = JSON.parse(body)
107
+ end
108
+
109
+ Response.new(
110
+ status: http_response.code.to_i,
111
+ reason_phrase: http_response.message,
112
+ headers: http_response.to_hash,
113
+ body: body
114
+ )
115
+ end
116
+
117
+ def raise_error!(response)
118
+ error_class = ERRORS.fetch(response.status) do
119
+ (response.status >= 500) ? ServerError : ClientError
120
+ end
121
+
122
+ raise error_class.new(response: response)
123
+ end
124
+
125
+ def log_request(http_request, uri)
126
+ return if logger.nil?
127
+
128
+ logger.info("request") { "#{http_request.method} #{uri}" }
129
+ logger.info("request") { format_headers(http_request.each_header.to_h) }
130
+ logger.info("request") { http_request.body } unless http_request.body.nil?
131
+ end
132
+
133
+ def log_response(response, http_response)
134
+ return if logger.nil?
135
+
136
+ logger.info("response") { "Status #{response.status}" }
137
+ logger.info("response") { format_headers(http_response.each_header.to_h) }
138
+ logger.info("response") { http_response.body } unless http_response.body.to_s.empty?
139
+ end
140
+
141
+ def format_headers(headers)
142
+ headers.map do |k, v|
143
+ "#{k}: #{k.match?(FILTERED_HEADERS) ? "[FILTERED]" : v.inspect}"
144
+ end.join("\n")
145
+ end
146
+ end
147
+ end