tesla 0.0.1 → 0.1.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.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +5 -0
  3. data/CHANGELOG.md +31 -0
  4. data/CONTRIBUTING.md +45 -0
  5. data/{LICENSE.txt → LICENSE.md} +1 -1
  6. data/README.md +366 -0
  7. data/SECURITY.md +38 -0
  8. data/lib/tesla/api/alert_endpoints.rb +35 -0
  9. data/lib/tesla/api/charging_endpoints.rb +115 -0
  10. data/lib/tesla/api/climate_endpoints.rb +180 -0
  11. data/lib/tesla/api/closure_endpoints.rb +142 -0
  12. data/lib/tesla/api/command_endpoints.rb +63 -0
  13. data/lib/tesla/api/media_endpoints.rb +114 -0
  14. data/lib/tesla/api/navigation_endpoints.rb +51 -0
  15. data/lib/tesla/api/oauth_endpoints.rb +118 -0
  16. data/lib/tesla/api/partner_endpoints.rb +62 -0
  17. data/lib/tesla/api/security_endpoints.rb +123 -0
  18. data/lib/tesla/api/software_endpoints.rb +38 -0
  19. data/lib/tesla/api/user_endpoints.rb +40 -0
  20. data/lib/tesla/api/vehicle_endpoints.rb +174 -0
  21. data/lib/tesla/api.rb +39 -0
  22. data/lib/tesla/authenticator.rb +33 -0
  23. data/lib/tesla/bearer_token_authenticator.rb +38 -0
  24. data/lib/tesla/certificate_options.rb +51 -0
  25. data/lib/tesla/charge_state.rb +111 -0
  26. data/lib/tesla/charging_site.rb +67 -0
  27. data/lib/tesla/client.rb +336 -0
  28. data/lib/tesla/client_credentials.rb +230 -0
  29. data/lib/tesla/climate_state.rb +116 -0
  30. data/lib/tesla/configuration.rb +483 -0
  31. data/lib/tesla/connection.rb +361 -0
  32. data/lib/tesla/connection_pool.rb +186 -0
  33. data/lib/tesla/deep_copy.rb +41 -0
  34. data/lib/tesla/drive_state.rb +80 -0
  35. data/lib/tesla/errors/bad_gateway.rb +9 -0
  36. data/lib/tesla/errors/bad_request.rb +9 -0
  37. data/lib/tesla/errors/client_error.rb +9 -0
  38. data/lib/tesla/errors/command_failed.rb +42 -0
  39. data/lib/tesla/errors/device_unexpected_response.rb +12 -0
  40. data/lib/tesla/errors/error.rb +7 -0
  41. data/lib/tesla/errors/forbidden.rb +14 -0
  42. data/lib/tesla/errors/gateway_timeout.rb +9 -0
  43. data/lib/tesla/errors/http_error.rb +89 -0
  44. data/lib/tesla/errors/internal_server_error.rb +9 -0
  45. data/lib/tesla/errors/invalid_response.rb +33 -0
  46. data/lib/tesla/errors/locked.rb +12 -0
  47. data/lib/tesla/errors/method_not_allowed.rb +9 -0
  48. data/lib/tesla/errors/misdirected_request.rb +13 -0
  49. data/lib/tesla/errors/network_error.rb +30 -0
  50. data/lib/tesla/errors/not_acceptable.rb +9 -0
  51. data/lib/tesla/errors/not_found.rb +9 -0
  52. data/lib/tesla/errors/oauth_error.rb +53 -0
  53. data/lib/tesla/errors/payment_required.rb +12 -0
  54. data/lib/tesla/errors/precondition_failed.rb +13 -0
  55. data/lib/tesla/errors/request_timeout.rb +13 -0
  56. data/lib/tesla/errors/server_error.rb +9 -0
  57. data/lib/tesla/errors/service_unavailable.rb +9 -0
  58. data/lib/tesla/errors/too_many_redirects.rb +9 -0
  59. data/lib/tesla/errors/too_many_requests.rb +9 -0
  60. data/lib/tesla/errors/unauthorized.rb +9 -0
  61. data/lib/tesla/errors/unavailable_for_legal_reasons.rb +12 -0
  62. data/lib/tesla/errors/unprocessable_content.rb +12 -0
  63. data/lib/tesla/errors/vehicle_asleep.rb +32 -0
  64. data/lib/tesla/gui_settings.rb +60 -0
  65. data/lib/tesla/idempotence.rb +30 -0
  66. data/lib/tesla/identifiers.rb +27 -0
  67. data/lib/tesla/json_parsing.rb +38 -0
  68. data/lib/tesla/nearby_charging_sites.rb +35 -0
  69. data/lib/tesla/path_escaping.rb +30 -0
  70. data/lib/tesla/redacted_output.rb +161 -0
  71. data/lib/tesla/redirect_handler.rb +196 -0
  72. data/lib/tesla/region.rb +27 -0
  73. data/lib/tesla/request_builder.rb +138 -0
  74. data/lib/tesla/resource.rb +403 -0
  75. data/lib/tesla/response_parser.rb +92 -0
  76. data/lib/tesla/retry_after.rb +34 -0
  77. data/lib/tesla/retry_handler.rb +252 -0
  78. data/lib/tesla/settings.rb +115 -0
  79. data/lib/tesla/url_validation.rb +131 -0
  80. data/lib/tesla/user.rb +36 -0
  81. data/lib/tesla/vehicle.rb +90 -0
  82. data/lib/tesla/vehicle_config.rb +95 -0
  83. data/lib/tesla/vehicle_data.rb +67 -0
  84. data/lib/tesla/vehicle_state.rb +125 -0
  85. data/lib/tesla/version.rb +6 -0
  86. data/lib/tesla.rb +139 -0
  87. data/sig/manifest.yaml +10 -0
  88. data/sig/tesla.rbs +989 -0
  89. metadata +134 -118
  90. data/.document +0 -5
  91. data/.rvmrc +0 -1
  92. data/Gemfile +0 -13
  93. data/Gemfile.lock +0 -20
  94. data/README.rdoc +0 -19
  95. data/Rakefile +0 -47
  96. data/test/helper.rb +0 -19
  97. data/test/test_tesla.rb +0 -7
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require_relative "certificate_options"
6
+ require_relative "connection_pool"
7
+ require_relative "errors/network_error"
8
+ require_relative "redacted_output"
9
+ require_relative "settings"
10
+ require_relative "url_validation"
11
+
12
+ module Tesla
13
+ # Manages HTTP connections to the Tesla Fleet API
14
+ #
15
+ # The connection of a client is its own, and cannot be given to it or read from it, so this class can change
16
+ # within 0.x as the rest of the internals can. Its settings are options of {Client} and of the {Tesla} module,
17
+ # such as `open_timeout` and `proxy_url`, which is where they are configured.
18
+ #
19
+ # TLS certificates are verified, and no option turns that off: a host whose certificate OpenSSL does not already
20
+ # trust, such as the vehicle command proxy with a certificate of its own, is reached by naming that certificate
21
+ # with `ca_file`.
22
+ #
23
+ # @api private
24
+ class Connection
25
+ include CertificateOptions
26
+ include Settings
27
+ include URLValidation
28
+
29
+ # Default timeout for opening connections in seconds
30
+ DEFAULT_OPEN_TIMEOUT = 60 # seconds
31
+ # Default timeout for reading responses in seconds
32
+ DEFAULT_READ_TIMEOUT = 60 # seconds
33
+ # Default timeout for writing requests in seconds
34
+ DEFAULT_WRITE_TIMEOUT = 60 # seconds
35
+ # Default seconds an idle connection is kept open for another request
36
+ DEFAULT_KEEP_ALIVE_TIMEOUT = 2 # seconds
37
+
38
+ # @!method open_timeout
39
+ # The timeout for opening connections in seconds
40
+ # @api private
41
+ # @return [Numeric] the timeout for opening connections in seconds
42
+ # @example Get the open timeout
43
+ # connection.open_timeout
44
+ # @!method open_timeout=(open_timeout)
45
+ # Set the timeout for opening connections in seconds
46
+ # @api private
47
+ # @param open_timeout [Numeric] the timeout for opening connections in seconds
48
+ # @return [void]
49
+ # @raise [ArgumentError] if it is not a number of seconds, in which case the timeout is left as it was
50
+ # @example Set the open timeout
51
+ # connection.open_timeout = 30
52
+ seconds_setting :open_timeout
53
+
54
+ # @!method read_timeout
55
+ # The timeout for reading responses in seconds
56
+ # @api private
57
+ # @return [Numeric] the timeout for reading responses in seconds
58
+ # @example Get the read timeout
59
+ # connection.read_timeout
60
+ # @!method read_timeout=(read_timeout)
61
+ # Set the timeout for reading responses in seconds
62
+ # @api private
63
+ # @param read_timeout [Numeric] the timeout for reading responses in seconds
64
+ # @return [void]
65
+ # @raise [ArgumentError] if it is not a number of seconds, in which case the timeout is left as it was
66
+ # @example Set the read timeout
67
+ # connection.read_timeout = 30
68
+ seconds_setting :read_timeout
69
+
70
+ # @!method write_timeout
71
+ # The timeout for writing requests in seconds
72
+ # @api private
73
+ # @return [Numeric] the timeout for writing requests in seconds
74
+ # @example Get the write timeout
75
+ # connection.write_timeout
76
+ # @!method write_timeout=(write_timeout)
77
+ # Set the timeout for writing requests in seconds
78
+ # @api private
79
+ # @param write_timeout [Numeric] the timeout for writing requests in seconds
80
+ # @return [void]
81
+ # @raise [ArgumentError] if it is not a number of seconds, in which case the timeout is left as it was
82
+ # @example Set the write timeout
83
+ # connection.write_timeout = 30
84
+ seconds_setting :write_timeout
85
+
86
+ # @!method keep_alive_timeout
87
+ # The seconds an idle connection is kept open for another request
88
+ #
89
+ # A request is sent on the connection kept open for its host when one was used within this many seconds, so
90
+ # that a series of requests does not open a connection each. Zero closes every connection when its request is
91
+ # done.
92
+ #
93
+ # @api private
94
+ # @return [Numeric] the seconds an idle connection is kept open
95
+ # @example Get the keep-alive timeout
96
+ # connection.keep_alive_timeout
97
+ # @!method keep_alive_timeout=(keep_alive_timeout)
98
+ # Set the seconds an idle connection is kept open for another request
99
+ # @api private
100
+ # @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open
101
+ # @return [void]
102
+ # @raise [ArgumentError] if it is not a number of seconds, in which case the timeout is left as it was
103
+ # @example Set the keep-alive timeout
104
+ # connection.keep_alive_timeout = 0
105
+ seconds_setting :keep_alive_timeout
106
+
107
+ # The IO object for debug output
108
+ #
109
+ # The credentials requests and responses carry are redacted from what is written to it (see {RedactedOutput}).
110
+ #
111
+ # @api private
112
+ # @return [IO, nil] the IO object for debug output
113
+ # @example Get or set the debug output
114
+ # connection.debug_output = $stderr
115
+ attr_accessor :debug_output
116
+
117
+ # The proxy URL for requests
118
+ # @api private
119
+ # @return [String, nil] the proxy URL for requests
120
+ # @example Get the proxy URL
121
+ # connection.proxy_url
122
+ attr_reader :proxy_url
123
+
124
+ # The parsed proxy URI
125
+ # @api private
126
+ # @return [URI::Generic, nil] the parsed proxy URI
127
+ # @example Get the proxy URI
128
+ # connection.proxy_uri
129
+ attr_reader :proxy_uri
130
+
131
+ # Summarize the connection for the console
132
+ #
133
+ # @api private
134
+ # @return [String] the summary, which includes the proxy URL without its user and password
135
+ # @example Inspect a connection
136
+ # connection.inspect # => #<Tesla::Connection proxy_url=nil open_timeout=60 read_timeout=60 write_timeout=60 keep_alive_timeout=2>
137
+ def inspect
138
+ "#<#{self.class} proxy_url=#{redact(proxy_url).inspect} open_timeout=#{open_timeout} " \
139
+ "read_timeout=#{read_timeout} write_timeout=#{write_timeout} keep_alive_timeout=#{keep_alive_timeout}>"
140
+ end
141
+
142
+ # Initialize a new connection
143
+ #
144
+ # @api private
145
+ # @param open_timeout [Numeric] the timeout for opening connections in seconds
146
+ # @param read_timeout [Numeric] the timeout for reading responses in seconds
147
+ # @param write_timeout [Numeric] the timeout for writing requests in seconds
148
+ # @param debug_output [IO, nil] the IO object for debug output
149
+ # @param proxy_url [String, nil] the proxy URL for requests
150
+ # @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open for another request
151
+ # @param ca_file [String, nil] the path of a file of certificates TLS is verified with
152
+ # @return [Connection] a new connection instance
153
+ # @raise [ArgumentError] if the proxy URL is invalid, or the certificate path names nothing
154
+ # @example Create a connection with default settings
155
+ # connection = Tesla::Connection.new
156
+ # @example Create a connection with custom timeouts
157
+ # connection = Tesla::Connection.new(open_timeout: 30, read_timeout: 30)
158
+ # @example Create a connection that trusts the certificate of the vehicle command proxy
159
+ # connection = Tesla::Connection.new(ca_file: "config/tls-cert.pem")
160
+ def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT,
161
+ write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: nil, proxy_url: nil,
162
+ keep_alive_timeout: DEFAULT_KEEP_ALIVE_TIMEOUT, ca_file: nil)
163
+ self.open_timeout = open_timeout
164
+ self.read_timeout = read_timeout
165
+ self.write_timeout = write_timeout
166
+ @debug_output = debug_output
167
+ self.keep_alive_timeout = keep_alive_timeout
168
+ @pool = ConnectionPool.new
169
+ self.proxy_url = proxy_url
170
+ self.ca_file = ca_file
171
+ end
172
+
173
+ # Perform an HTTP request
174
+ #
175
+ # The request is sent on the connection kept open for its host, when there is one it can be sent on, and that
176
+ # connection is kept open for the next request (see {#keep_alive_timeout}).
177
+ #
178
+ # The settings are read once, before the connection is taken, and a connection that has to be opened is opened
179
+ # with them and kept under them, so that a setting changed while the request is being sent, from another thread,
180
+ # closes the connection when the next request is sent rather than keeping it as though it had been opened with
181
+ # the setting as it is now, and a connection is never opened with one setting and kept under another.
182
+ #
183
+ # @api private
184
+ # @param request [Net::HTTPRequest] the HTTP request to perform
185
+ # @return [Net::HTTPResponse] the HTTP response
186
+ # @raise [NetworkError] if a network error occurs
187
+ # @example Perform a request
188
+ # response = connection.perform(request: request)
189
+ def perform(request:)
190
+ send_request(request, settings_for(request.uri))
191
+ rescue *NetworkError::WRAPPED => e
192
+ raise NetworkError, "Network error: #{e}"
193
+ end
194
+
195
+ # Close the connections kept open for the next request
196
+ #
197
+ # The connections are opened again as they are needed, so a connection that is closed while it is being used is
198
+ # not interrupted, and requests can still be made afterwards.
199
+ #
200
+ # @api private
201
+ # @return [Connection] the connection
202
+ # @example Close the connections a client keeps open
203
+ # connection.close
204
+ def close
205
+ pool.close
206
+ self
207
+ end
208
+
209
+ # Set the proxy URL for requests
210
+ #
211
+ # @api private
212
+ # @param proxy_url [String, nil] the proxy URL, or nil to read proxies from the environment again
213
+ # @return [void]
214
+ # @raise [ArgumentError] if the proxy URL is invalid, in which case the proxy is left as it was
215
+ # @example Set the proxy URL
216
+ # connection.proxy_url = "http://proxy.example.com:8080"
217
+ # @example Clear the proxy URL
218
+ # connection.proxy_url = nil
219
+ def proxy_url=(proxy_url)
220
+ proxy_uri = proxy_url && parse_proxy_uri(proxy_url)
221
+ @proxy_url = proxy_url
222
+ @proxy_uri = proxy_uri
223
+ end
224
+
225
+ private
226
+
227
+ # The connections kept open for the next request
228
+ # @api private
229
+ # @return [ConnectionPool] the pool
230
+ attr_reader :pool
231
+
232
+ # The settings a connection to a URI is opened with
233
+ #
234
+ # A connection kept open must still have them to be reused. The proxy is the one a request to the URI is sent
235
+ # through, which is read from the http_proxy, https_proxy, and no_proxy environment variables when no proxy URL
236
+ # is configured, so that a connection kept open is not reused past a change to the environment either.
237
+ #
238
+ # @api private
239
+ # @param uri [URI::Generic] the URI a request is sent to
240
+ # @return [Hash{Symbol => Object}] the settings
241
+ def settings_for(uri)
242
+ {open_timeout:, read_timeout:, write_timeout:, keep_alive_timeout:, debug_output:,
243
+ proxy: proxy_uri || uri.find_proxy, ca_file:}
244
+ end
245
+
246
+ # Send a request on a connection opened with the settings given
247
+ #
248
+ # A connection that has to be opened is opened with those settings, and the connection is kept under them
249
+ # afterwards, rather than under the settings as they are then.
250
+ #
251
+ # A request sent on a connection with a debug output asks for its response uncompressed, since the debug output
252
+ # is written the body as it was sent, and the credentials of a compressed body could not be redacted from it
253
+ # (see {RedactedOutput}).
254
+ #
255
+ # @api private
256
+ # @param request [Net::HTTPRequest] the HTTP request to send
257
+ # @param opened_with [Hash{Symbol => Object}] the settings a connection must have been opened with to send it on
258
+ # @return [Net::HTTPResponse] the HTTP response
259
+ def send_request(request, opened_with)
260
+ kept_for = opened_with.fetch(:keep_alive_timeout)
261
+ request["Accept-Encoding"] = "identity" if opened_with.fetch(:debug_output)
262
+ http_client = pool.checkout(request:, settings: opened_with, keep_alive_timeout: kept_for) do
263
+ build_http_client(request.uri, opened_with)
264
+ end
265
+ response = request_on(http_client, request)
266
+ pool.checkin(request:, http_client:, settings: opened_with, keep_alive_timeout: kept_for)
267
+ response
268
+ end
269
+
270
+ # Send a request on a connection, and close the connection if the request raises
271
+ #
272
+ # The connection is closed whatever the request raises, rather than left open with a response half read: a
273
+ # timeout of the caller's own, a Thread#raise, or an interrupt stops a request as surely as the network does.
274
+ #
275
+ # @api private
276
+ # @param http_client [Net::HTTP] the connection
277
+ # @param request [Net::HTTPRequest] the HTTP request to send
278
+ # @return [Net::HTTPResponse] the HTTP response
279
+ def request_on(http_client, request)
280
+ http_client.request(request)
281
+ rescue Exception # rubocop:disable Lint/RescueException
282
+ pool.discard(http_client)
283
+ raise
284
+ end
285
+
286
+ # Decode a percent-encoded component of a URL
287
+ # @api private
288
+ # @param component [String, nil] the component
289
+ # @return [String, nil] the decoded component, or nil for nil
290
+ def decode(component)
291
+ component && URI.decode_uri_component(component)
292
+ end
293
+
294
+ # Build an HTTP client for the given URI, with the settings given
295
+ #
296
+ # An https:// proxy is connected to over TLS.
297
+ #
298
+ # @api private
299
+ # @param uri [URI::Generic] the URI to connect to
300
+ # @param opened_with [Hash{Symbol => Object}] the settings to open the connection with
301
+ # @return [Net::HTTP] the HTTP client
302
+ # @raise [ArgumentError] if the URI has no host
303
+ def build_http_client(uri, opened_with)
304
+ host = uri.host
305
+ raise ArgumentError, "URI has no host: #{uri}" if host.nil?
306
+
307
+ proxy_host, proxy_port, proxy_user, proxy_pass, proxy_use_ssl = proxy_arguments_for(opened_with.fetch(:proxy))
308
+ http_client = Net::HTTP.new(host, uri.port, proxy_host, proxy_port, proxy_user, proxy_pass, nil, proxy_use_ssl)
309
+ http_client.use_ssl = uri.scheme.eql?("https")
310
+ configure_http_client(http_client, opened_with)
311
+ end
312
+
313
+ # The proxy host, port, user, password, and whether to use TLS for a proxy
314
+ #
315
+ # The user and password are decoded from the proxy URL, and an https:// proxy is connected to over TLS.
316
+ #
317
+ # @api private
318
+ # @param proxy [URI::Generic, nil] the proxy, or nil to connect directly
319
+ # @return [Array] the proxy host, port, user, password, and whether to use TLS, each nil without a proxy
320
+ def proxy_arguments_for(proxy)
321
+ [proxy&.host, proxy&.port, decode(proxy&.user), decode(proxy&.password), proxy&.instance_of?(URI::HTTPS)]
322
+ end
323
+
324
+ # Configure an HTTP client with the timeouts given
325
+ #
326
+ # @api private
327
+ # @param http_client [Net::HTTP] the HTTP client to configure
328
+ # @param opened_with [Hash{Symbol => Object}] the settings to configure it with
329
+ # @return [Net::HTTP] the configured HTTP client
330
+ def configure_timeouts(http_client, opened_with)
331
+ http_client.tap do |c|
332
+ c.open_timeout = opened_with.fetch(:open_timeout)
333
+ c.read_timeout = opened_with.fetch(:read_timeout)
334
+ c.write_timeout = opened_with.fetch(:write_timeout)
335
+ c.keep_alive_timeout = opened_with.fetch(:keep_alive_timeout)
336
+ end
337
+ end
338
+
339
+ # Configure an HTTP client with the timeouts, debug output, and CA file given
340
+ #
341
+ # The debug output is wrapped, so that the credentials Net::HTTP writes with the headers of a request do not
342
+ # reach the IO.
343
+ #
344
+ # Net::HTTP sends an idempotent request again once on its own when the connection it was sent on fails, which
345
+ # the retries of {RetryHandler} would otherwise come on top of, so a request that timed out reading its response
346
+ # would wait twice as many read timeouts as `max_retries` allows for. It is left to {RetryHandler} alone.
347
+ #
348
+ # @api private
349
+ # @param http_client [Net::HTTP] the HTTP client to configure
350
+ # @param opened_with [Hash{Symbol => Object}] the settings to configure it with
351
+ # @return [Net::HTTP] the configured HTTP client
352
+ def configure_http_client(http_client, opened_with)
353
+ debug_output = opened_with.fetch(:debug_output)
354
+ configure_timeouts(http_client, opened_with).tap do |c|
355
+ c.max_retries = 0
356
+ c.set_debug_output(debug_output && RedactedOutput.new(debug_output))
357
+ c.ca_file = opened_with.fetch(:ca_file)
358
+ end
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require_relative "idempotence"
5
+
6
+ module Tesla
7
+ # Keeps the connections a {Connection} is not using, so that a request can be sent on one that is already open
8
+ #
9
+ # One connection is kept for each host, so that requests made at the same time are sent on connections of their
10
+ # own rather than waiting for one another, and the connections they opened are closed when they are given back.
11
+ # A connection opened with settings that have since changed is closed rather than reused.
12
+ #
13
+ # A process forked from one that kept a connection open shares the socket of that connection with it, so the
14
+ # connection is kept for the process that opened it alone: a forked process opens one of its own, rather than
15
+ # reading the responses to the requests of the other process, and leaves the one it inherited alone, rather than
16
+ # closing it under the process still using it.
17
+ #
18
+ # @api private
19
+ class ConnectionPool
20
+ include Idempotence
21
+
22
+ # Initialize a new ConnectionPool
23
+ #
24
+ # @api private
25
+ # @return [ConnectionPool] a new instance
26
+ # @example Create a connection pool
27
+ # pool = Tesla::ConnectionPool.new
28
+ def initialize
29
+ @connections = {}
30
+ @mutex = Mutex.new
31
+ end
32
+
33
+ # The connection to send a request on
34
+ #
35
+ # A connection kept open for the host of the request is used when the request can be sent on one, and one is
36
+ # opened with the block otherwise.
37
+ #
38
+ # @api private
39
+ # @param request [Net::HTTPRequest] the request
40
+ # @param settings [Object] the settings a connection must have been opened with to be reused
41
+ # @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open, or zero to open one each time
42
+ # @yield the connection to open when none is kept for the host of the request
43
+ # @return [Net::HTTP] the connection, which is open
44
+ # @example Take the connection to send a request on
45
+ # pool.checkout(request:, settings:, keep_alive_timeout: 2) { Net::HTTP.new(uri.host, uri.port) }
46
+ def checkout(request:, settings:, keep_alive_timeout:)
47
+ http_client = take(pool_key(request), settings) if keep_alive?(request, keep_alive_timeout)
48
+ http_client || yield.tap(&:start)
49
+ end
50
+
51
+ # Keep the connection a request was sent on, or close it
52
+ #
53
+ # @api private
54
+ # @param request [Net::HTTPRequest] the request
55
+ # @param http_client [Net::HTTP] the connection it was sent on
56
+ # @param settings [Object] the settings the connection was opened with
57
+ # @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open, or zero to close it
58
+ # @return [void]
59
+ # @example Give back the connection a request was sent on
60
+ # pool.checkin(request:, http_client:, settings:, keep_alive_timeout: 2)
61
+ def checkin(request:, http_client:, settings:, keep_alive_timeout:)
62
+ return discard(http_client) unless keep_alive?(request, keep_alive_timeout)
63
+
64
+ store(pool_key(request), http_client, settings)
65
+ end
66
+
67
+ # Close a connection
68
+ #
69
+ # @api private
70
+ # @param http_client [Net::HTTP, nil] the connection, or nil when none was opened
71
+ # @return [void]
72
+ # @example Close the connection a request failed on
73
+ # pool.discard(http_client)
74
+ def discard(http_client)
75
+ http_client.finish if http_client&.started?
76
+ end
77
+
78
+ # Take the connection kept for a key
79
+ #
80
+ # The connection is removed from the pool, so that it is used by one request at a time, and is given back with
81
+ # {#store}. A connection opened with other settings is closed instead of returned, and one opened by the process
82
+ # this one was forked from is left to that process.
83
+ #
84
+ # @api private
85
+ # @param key [Object] the key the connection is kept under
86
+ # @param settings [Object] the settings a connection must have been opened with to be reused
87
+ # @return [Net::HTTP, nil] the connection, or nil when none is kept for the key
88
+ # @example Take the connection kept for a host
89
+ # pool.take(["https", "fleet-api.prd.na.vn.cloud.tesla.com", 443], settings)
90
+ def take(key, settings)
91
+ http_client, kept_settings, pid = @mutex.synchronize { @connections.delete(key) }
92
+ return unless opened_here?(pid)
93
+ return http_client if settings.eql?(kept_settings)
94
+
95
+ discard(http_client)
96
+ nil
97
+ end
98
+
99
+ # Keep a connection for a key
100
+ #
101
+ # A connection is kept only when none is kept for the key already, so that the pool holds one connection per
102
+ # host; a connection it cannot keep is closed. One kept by the process this one was forked from does not count,
103
+ # and is replaced.
104
+ #
105
+ # @api private
106
+ # @param key [Object] the key to keep the connection under
107
+ # @param http_client [Net::HTTP] the connection
108
+ # @param settings [Object] the settings the connection was opened with
109
+ # @return [Boolean] whether the connection was kept
110
+ # @example Keep a connection for a host
111
+ # pool.store(["https", "fleet-api.prd.na.vn.cloud.tesla.com", 443], http_client, settings)
112
+ def store(key, http_client, settings)
113
+ stored = @mutex.synchronize do
114
+ @connections[key] = [http_client, settings, Process.pid] unless opened_here?(@connections.dig(key, 2))
115
+ end
116
+ discard(http_client) if stored.nil?
117
+ !stored.nil?
118
+ end
119
+
120
+ # Close every connection the pool keeps
121
+ #
122
+ # The keys are read under the lock, as every other read and write of the connections is, since a request giving
123
+ # its connection back at that moment writes to them from another thread. The connections are then taken one at a
124
+ # time, so that a connection given back while the pool is closing is either closed with the rest or kept for the
125
+ # next request, rather than dropped without being closed. The connections opened by the process this one was
126
+ # forked from are forgotten rather than closed, since that process may still be using them.
127
+ #
128
+ # @api private
129
+ # @return [ConnectionPool] the pool
130
+ # @example Close the connections of a pool
131
+ # pool.close
132
+ def close
133
+ keys = @mutex.synchronize { @connections.keys }
134
+ keys.each { |key| discard(delete(key)) }
135
+ self
136
+ end
137
+
138
+ private
139
+
140
+ # Take the connection kept for a key, whatever settings it was opened with
141
+ # @api private
142
+ # @param key [Object] the key the connection is kept under
143
+ # @return [Net::HTTP, nil] the connection, or nil when none is kept for the key or the one kept was opened by the
144
+ # process this one was forked from
145
+ def delete(key)
146
+ http_client, _settings, pid = @mutex.synchronize { @connections.delete(key) }
147
+ http_client if opened_here?(pid)
148
+ end
149
+
150
+ # Whether a connection was opened by this process
151
+ #
152
+ # A connection opened by the process this one was forked from was not, and shares its socket with that process.
153
+ #
154
+ # @api private
155
+ # @param pid [Integer, nil] the ID of the process that opened the connection, or nil when none is kept
156
+ # @return [Boolean] whether it is the ID of this process
157
+ def opened_here?(pid)
158
+ Process.pid.eql?(pid)
159
+ end
160
+
161
+ # Whether a request can be sent on a connection that is kept open
162
+ #
163
+ # The connection it is sent on is kept open afterwards too. A request that asks the server to do something is
164
+ # sent on a connection of its own and that connection is closed afterwards: Net::HTTP reconnects before it reuses
165
+ # a connection the server has closed, and {RetryHandler} sends a safe request whose connection breaks again, but
166
+ # a request that asks the server to do something cannot be sent again to find out whether the server acted on
167
+ # the first one.
168
+ #
169
+ # @api private
170
+ # @param request [Net::HTTPRequest] the request
171
+ # @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open
172
+ # @return [Boolean] whether the connection is kept open
173
+ def keep_alive?(request, keep_alive_timeout)
174
+ keep_alive_timeout.positive? && safe?(request.method)
175
+ end
176
+
177
+ # The key a connection is kept under, which is the host it is open to
178
+ # @api private
179
+ # @param request [Net::HTTPRequest] the request
180
+ # @return [Array] the scheme, host, and port
181
+ def pool_key(request)
182
+ uri = request.uri
183
+ [uri.scheme, uri.host, uri.port]
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tesla
4
+ # The copies a {Resource} is made of and answers with: frozen for the resource, and unfrozen for the caller
5
+ # @api private
6
+ module DeepCopy
7
+ private
8
+
9
+ # Copy a value, freezing the copy and everything nested inside it
10
+ #
11
+ # The keys of hashes are converted to strings. Everything a parsed JSON response holds other than hashes, arrays, and strings is already immutable.
12
+ #
13
+ # @api private
14
+ # @param value [Object] the value to copy
15
+ # @return [Object] the frozen copy
16
+ def deep_freeze(value)
17
+ case value
18
+ when Hash then value.to_h { |key, nested| [key.to_s, deep_freeze(nested)] }.freeze
19
+ when Array then value.map { |nested| deep_freeze(nested) }.freeze
20
+ when String then value.dup.freeze
21
+ else value
22
+ end
23
+ end
24
+
25
+ # Copy a value and everything nested inside it, for the caller to change
26
+ #
27
+ # The keys of hashes are left as they are, since a hash freezes the strings it is keyed by itself. Everything a
28
+ # parsed JSON response holds other than hashes, arrays, and strings answers `dup` with itself.
29
+ #
30
+ # @api private
31
+ # @param value [Object] the value to copy
32
+ # @return [Object] the copy, which is not frozen
33
+ def deep_dup(value)
34
+ case value
35
+ when Hash then value.transform_values { |nested| deep_dup(nested) }
36
+ when Array then value.map { |nested| deep_dup(nested) }
37
+ else value.dup
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+
5
+ module Tesla
6
+ # The position and the driving state of a vehicle
7
+ # @api public
8
+ class DriveState < Resource
9
+ inspect_with :latitude, :longitude, :shift_state
10
+
11
+ # @!method latitude
12
+ # The latitude of the vehicle
13
+ #
14
+ # The Fleet API answers with it when it is asked for "location_data", for a token with the vehicle_location scope.
15
+ #
16
+ # @api public
17
+ # @return [Float, nil] the latitude of the vehicle
18
+ # @example
19
+ # drive_state.latitude
20
+ attribute :latitude
21
+
22
+ # @!method longitude
23
+ # The longitude of the vehicle
24
+ #
25
+ # The Fleet API answers with it when it is asked for "location_data", for a token with the vehicle_location scope.
26
+ #
27
+ # @api public
28
+ # @return [Float, nil] the longitude of the vehicle
29
+ # @example
30
+ # drive_state.longitude
31
+ attribute :longitude
32
+
33
+ # @!method heading
34
+ # The compass heading of the vehicle in degrees
35
+ # @api public
36
+ # @return [Integer, nil] the compass heading of the vehicle
37
+ # @example
38
+ # drive_state.heading
39
+ attribute :heading
40
+
41
+ # @!method speed
42
+ # The speed of the vehicle in miles per hour, or nil when it is parked
43
+ # @api public
44
+ # @return [Integer, nil] the speed of the vehicle
45
+ # @example
46
+ # drive_state.speed
47
+ attribute :speed
48
+
49
+ # @!method power
50
+ # The power the vehicle is drawing in kilowatts
51
+ #
52
+ # It is negative while the vehicle charges or regenerates.
53
+ #
54
+ # @api public
55
+ # @return [Integer, nil] the power the vehicle is drawing
56
+ # @example
57
+ # drive_state.power
58
+ attribute :power
59
+
60
+ # @!method shift_state
61
+ # The gear the vehicle is in, "P", "R", "N", or "D"
62
+ #
63
+ # It is nil when the vehicle is parked and idle.
64
+ #
65
+ # @api public
66
+ # @return [String, nil] the gear the vehicle is in
67
+ # @example
68
+ # drive_state.shift_state
69
+ attribute :shift_state
70
+
71
+ # @!method timestamp
72
+ # The time the vehicle reported the state
73
+ # @api public
74
+ # @return [Time, nil] the time the vehicle reported the state
75
+ # @raise [InvalidResponse] if the timestamp is not a number of milliseconds
76
+ # @example
77
+ # drive_state.timestamp
78
+ time_attribute :timestamp
79
+ end
80
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "server_error"
4
+
5
+ module Tesla
6
+ # Error raised for HTTP 502 Bad Gateway responses
7
+ # @api public
8
+ class BadGateway < ServerError; end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client_error"
4
+
5
+ module Tesla
6
+ # Error raised for HTTP 400 Bad Request responses
7
+ # @api public
8
+ class BadRequest < ClientError; end
9
+ end