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,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require_relative "errors/network_error"
5
+ require_relative "retry_after"
6
+ require_relative "settings"
7
+
8
+ module Tesla
9
+ # Sends a request again when the server, or the network, turns it away
10
+ #
11
+ # A 429 Too Many Requests says a rate limiter turned the request away before it reached the endpoint, so sending it
12
+ # again is safe for any request, even one that is not idempotent, such as a command: the vehicle never saw the
13
+ # attempt before it.
14
+ #
15
+ # A 502 Bad Gateway, a 503 Service Unavailable, a 504 Gateway Timeout, and a request lost to the network say less.
16
+ # A 502 or 504 comes from a gateway that could not read an answer from the origin behind it, a 503 can come from a
17
+ # service that gave up waiting on a vehicle still working on the request, and a request lost to the network may
18
+ # have arrived and been acted on before its answer went missing. The vehicle may have acted on the request in
19
+ # each case, so a request that asks it to do something, such as honking the horn or opening the trunk, is not sent
20
+ # again for one: a trunk asked to open a second time after it opened closes again on the vehicles that can close
21
+ # it.
22
+ #
23
+ # Whether a request is one of those is the caller's to say, with the `retry_unanswered` argument of {#handle}:
24
+ # {Client} says whether the method of the request is safe, since a command cannot be sent a second time to find
25
+ # out whether the vehicle acted on the first. A 429 is sent again whatever the caller says, since no request
26
+ # reached the endpoint.
27
+ #
28
+ # A request is sent again twice by default, which is enough for the moment of rate limiting or the lost
29
+ # connection that a retry is for, and {#max_retries} of zero turns retrying off, so that a request that was
30
+ # turned away raises {TooManyRequests}, {BadGateway}, {ServiceUnavailable}, {GatewayTimeout}, or {NetworkError}
31
+ # rather than pausing the caller's thread. A response asking to wait longer than {#max_retry_delay} raises
32
+ # whatever {#max_retries} is, so the wait a caller can be held for is theirs to cap.
33
+ #
34
+ # @api private
35
+ class RetryHandler
36
+ include RetryAfter
37
+ include Settings
38
+
39
+ # Default number of times a request is sent again
40
+ DEFAULT_MAX_RETRIES = 2
41
+ # Default longest a request waits before it is sent again, in seconds
42
+ DEFAULT_MAX_RETRY_DELAY = 60 # seconds
43
+ # The status that says a rate limiter turned the request away before it reached the endpoint
44
+ THROTTLED_STATUS = 429
45
+ # The statuses that say a gateway read no answer from the origin, which may have acted on the request
46
+ UNANSWERED_STATUSES = [502, 503, 504].freeze
47
+ private_constant :THROTTLED_STATUS, :UNANSWERED_STATUSES
48
+
49
+ # @!method max_retries
50
+ # The number of times a request is sent again
51
+ # @api private
52
+ # @return [Integer] the number of times a request is sent again
53
+ # @example Get the maximum retries
54
+ # handler.max_retries
55
+ # @!method max_retries=(max_retries)
56
+ # Set the number of times a request is sent again
57
+ # @api private
58
+ # @param max_retries [Integer] the number of times a request is sent again
59
+ # @return [void]
60
+ # @raise [ArgumentError] if it is not a whole number of times, in which case the maximum is left as it was
61
+ # @example Set the maximum retries
62
+ # handler.max_retries = 3
63
+ count_setting :max_retries
64
+
65
+ # @!method max_retry_delay
66
+ # The longest a request waits before it is sent again, in seconds
67
+ # @api private
68
+ # @return [Numeric] the longest a request waits before it is sent again, in seconds
69
+ # @example Get the maximum retry delay
70
+ # handler.max_retry_delay
71
+ # @!method max_retry_delay=(max_retry_delay)
72
+ # Set the longest a request waits before it is sent again, in seconds
73
+ # @api private
74
+ # @param max_retry_delay [Numeric] the longest a request waits before it is sent again, in seconds
75
+ # @return [void]
76
+ # @raise [ArgumentError] if it is not a number of seconds, in which case the maximum is left as it was
77
+ # @example Set the maximum retry delay
78
+ # handler.max_retry_delay = 30
79
+ seconds_setting :max_retry_delay
80
+
81
+ # The source of the randomness the backoff is jittered with
82
+ # @api private
83
+ # @return [Random, Class<Random>] the source of randomness
84
+ # @example Get the source of randomness
85
+ # handler.random
86
+ attr_reader :random
87
+
88
+ # Initialize a new RetryHandler
89
+ #
90
+ # @api private
91
+ # @param max_retries [Integer] the number of times a request is sent again
92
+ # @param max_retry_delay [Numeric] the longest a request waits before it is sent again, in seconds
93
+ # @param random [Random, Class<Random>] the source of the randomness the backoff is jittered with, which
94
+ # answers `rand` with a Float between zero and one
95
+ # @return [RetryHandler] a new instance
96
+ # @example Create a retry handler
97
+ # handler = Tesla::RetryHandler.new(max_retries: 3)
98
+ def initialize(max_retries: DEFAULT_MAX_RETRIES, max_retry_delay: DEFAULT_MAX_RETRY_DELAY, random: Random)
99
+ self.max_retries = max_retries
100
+ self.max_retry_delay = max_retry_delay
101
+ @random = random
102
+ end
103
+
104
+ # Send a request until the server and the network stop turning it away
105
+ #
106
+ # The block is called again for each retry, so a request that was redirected is followed again from the start.
107
+ # The wait between attempts is the one the `Retry-After` header of the response asks for, and doubles from one
108
+ # second otherwise, up to {#max_retry_delay}, jittered as {#backoff} describes; that is the wait after a
109
+ # {NetworkError} too, since a request that never reached the server has no response to read a wait from. A
110
+ # response that asks to wait longer than {#max_retry_delay} is returned rather than waited for, so that the
111
+ # caller is told what happened instead of pausing for as long as the server likes, and a {NetworkError} that
112
+ # has no retry left is raised as it was.
113
+ #
114
+ # A request a rate limiter turned away is told apart from one the origin may have acted on, since they are not
115
+ # equally safe to send again: a 429 says the request never reached the endpoint, where a 502, 503, or 504, or a
116
+ # request lost to the network, may have been acted on before the answer went missing. The caller says whether
117
+ # its request is safe to send again for those. A 429 is sent again for every request.
118
+ #
119
+ # The request is not named, since the block builds one of its own each time it is called, so that a body read
120
+ # as a stream is sent from the start (see {Client#perform}).
121
+ #
122
+ # @api private
123
+ # @param retry_unanswered [Boolean] whether a request the origin may have acted on is sent again
124
+ # @yield the response, each time the request is sent
125
+ # @return [Net::HTTPResponse] the last response
126
+ # @raise [NetworkError] if the request is lost to the network and is not sent again
127
+ # @example Send a safe request, which is safe to send again whatever happened to it
128
+ # handler.handle(retry_unanswered: true) { connection.perform(request:) }
129
+ # @example Send a request that is safe to send again only when a rate limiter turned it away
130
+ # handler.handle(retry_unanswered: false) { connection.perform(request:) }
131
+ def handle(retry_unanswered:)
132
+ retries = 0
133
+ loop do
134
+ response = attempt(retries:, retry_unanswered:) { yield }
135
+ return response if response
136
+
137
+ retries += 1
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ # Send a request once, waiting afterwards when it is to be sent again
144
+ #
145
+ # @api private
146
+ # @param retries [Integer] the number of times the request has been sent again
147
+ # @param retry_unanswered [Boolean] whether a request the origin may have acted on is sent again
148
+ # @yield the response to the request
149
+ # @return [Net::HTTPResponse, nil] the response, or nil once the wait before sending the request again is over
150
+ # @raise [NetworkError] if the request is lost to the network and is not sent again
151
+ def attempt(retries:, retry_unanswered:)
152
+ response = yield
153
+ delay = retry_delay(response:, retries:, unanswered: retry_unanswered)
154
+ delay ? wait(delay) : response
155
+ rescue NetworkError
156
+ delay = network_retry_delay(retries:, allowed: retry_unanswered)
157
+ raise unless delay
158
+
159
+ wait(delay)
160
+ end
161
+
162
+ # Wait before a request is sent again
163
+ #
164
+ # Kernel#sleep takes any Numeric, but its signature asks for the narrower interface Integer and Float happen to
165
+ # answer, so the seconds are passed to it unchecked rather than narrowing what {#max_retry_delay} accepts.
166
+ #
167
+ # @api private
168
+ # @param delay [Numeric] the seconds to wait
169
+ # @return [nil] nothing, so that the caller sends the request again
170
+ def wait(delay)
171
+ sleep(delay) # steep:ignore UnresolvedOverloading
172
+ nil
173
+ end
174
+
175
+ # The seconds to wait before sending a request again
176
+ #
177
+ # A wait the response asked for is given up on when it is longer than {#max_retry_delay}, where the wait the
178
+ # handler chose for itself is shortened to it, since a backoff that has grown past the cap is the handler
179
+ # asking to wait longer than the caller allowed rather than the server doing so.
180
+ #
181
+ # @api private
182
+ # @param response [Net::HTTPResponse] the response to the request
183
+ # @param retries [Integer] the number of times the request has been sent again
184
+ # @param unanswered [Boolean] whether a request the origin may have acted on is sent again
185
+ # @return [Numeric, nil] the seconds to wait, or nil when the request is not sent again
186
+ def retry_delay(response:, retries:, unanswered:)
187
+ return unless retryable?(retry?(response, unanswered), retries)
188
+
189
+ retry_after = retry_after_of(response)
190
+ retry_after ? capped(retry_after) : backoff(retries)
191
+ end
192
+
193
+ # The seconds to wait before sending a request again after a network error
194
+ # @api private
195
+ # @param retries [Integer] the number of times the request has been sent again
196
+ # @param allowed [Boolean] whether a request lost to the network is sent again
197
+ # @return [Numeric, nil] the seconds to wait, or nil when the request is not sent again
198
+ def network_retry_delay(retries:, allowed:)
199
+ return unless retryable?(allowed, retries)
200
+
201
+ backoff(retries)
202
+ end
203
+
204
+ # Whether a request has a retry left and is one the caller allows to be sent again
205
+ # @api private
206
+ # @param allowed [Boolean] whether the request is sent again for what happened to it
207
+ # @param retries [Integer] the number of times the request has been sent again
208
+ # @return [Boolean] whether the request is sent again
209
+ def retryable?(allowed, retries)
210
+ retries < max_retries && allowed
211
+ end
212
+
213
+ # The seconds a response asked for, unless longer than the caller allowed
214
+ # @api private
215
+ # @param delay [Integer] the seconds the response asked to wait
216
+ # @return [Integer, nil] the seconds to wait, or nil when they are longer than {#max_retry_delay}
217
+ def capped(delay)
218
+ delay unless delay > max_retry_delay
219
+ end
220
+
221
+ # Whether a response is one the request is sent again for
222
+ #
223
+ # A 429 says a rate limiter turned the request away before it reached the endpoint, and is sent again whatever
224
+ # the request; a 502, 503, or 504 says the origin may have acted on it, and is sent again only by the caller
225
+ # that says its request can be answered twice.
226
+ #
227
+ # @api private
228
+ # @param response [Net::HTTPResponse] the response to the request
229
+ # @param unanswered [Boolean] whether a request the origin may have acted on is sent again
230
+ # @return [Boolean] whether the request is sent again
231
+ def retry?(response, unanswered)
232
+ status = Integer(response.code)
233
+ status.eql?(THROTTLED_STATUS) || (unanswered && UNANSWERED_STATUSES.include?(status))
234
+ end
235
+
236
+ # The seconds to wait when the response does not ask for a wait of its own
237
+ #
238
+ # The wait doubles with each retry up to {#max_retry_delay}, and is then jittered down by up to half of itself,
239
+ # so that the clients a server turned away at the same moment do not all send their requests again at the same
240
+ # instant and turn the retry into a second wave of the load the server was shedding. A wait the response asked
241
+ # for is not jittered: the server named the moment it is ready for the request, and jittering that wait down
242
+ # would send the request again before then.
243
+ #
244
+ # @api private
245
+ # @param retries [Integer] the number of times the request has been sent again
246
+ # @return [Numeric] the seconds to wait, between half of the doubling wait and all of it
247
+ def backoff(retries)
248
+ delay = [2**retries, max_retry_delay].min
249
+ delay - (random.rand * delay / 2) #: Numeric
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tesla
4
+ # Declares the settings that are a number of seconds, a number of times, or a credential the environment can
5
+ # carry, and checks what is assigned to them, mixed into the configuration, the connection, and the retry handler
6
+ #
7
+ # A number is checked where it is assigned, as a host, a proxy URL, and the path of a certificate are (see
8
+ # {URLValidation} and {CertificateOptions}), so that the error names the assignment that was wrong rather than
9
+ # the request that later waits for a negative number of seconds, or compares the times it has been sent again
10
+ # with a String. The value that is rejected is left as it was.
11
+ #
12
+ # The settings are declared rather than written out, as the readers of a {Resource} are, since the reader and the
13
+ # writer of each differ only in the name they carry and the check the writer makes; each is documented where it
14
+ # is declared.
15
+ #
16
+ # @api private
17
+ module Settings
18
+ # The macros that declare the settings, extended into whatever includes {Settings}
19
+ # @api private
20
+ module Declaration
21
+ # Declare a setting that is a number of seconds
22
+ #
23
+ # @api private
24
+ # @param name [Symbol] the name of the setting
25
+ # @return [Symbol] the name of the writer
26
+ # @example Declare a timeout
27
+ # seconds_setting :open_timeout
28
+ def seconds_setting(name)
29
+ attr_reader(name)
30
+
31
+ define_method(:"#{name}=") do |seconds|
32
+ # @type self: Settings
33
+ instance_variable_set(:"@#{name}", validate_number(name, seconds))
34
+ end
35
+ end
36
+
37
+ # Declare a setting that is a number of times
38
+ #
39
+ # @api private
40
+ # @param name [Symbol] the name of the setting
41
+ # @return [Symbol] the name of the writer
42
+ # @example Declare a maximum
43
+ # count_setting :max_retries
44
+ def count_setting(name)
45
+ attr_reader(name)
46
+
47
+ define_method(:"#{name}=") do |count|
48
+ # @type self: Settings
49
+ instance_variable_set(:"@#{name}", validate_count(name, count))
50
+ end
51
+ end
52
+
53
+ # Declare a setting that falls back to an environment variable
54
+ #
55
+ # The environment is read each time the setting is, rather than once as the library is loaded, so that a
56
+ # variable set afterwards, such as one a dotenv file is read for, is the one a client is built with. A setting
57
+ # that has been assigned, even nil, is read as it was assigned.
58
+ #
59
+ # @api private
60
+ # @param name [Symbol] the name of the setting
61
+ # @param variable [String] the name of the environment variable the setting falls back to
62
+ # @return [Array<Symbol>] the name of the writer
63
+ # @example Declare a credential
64
+ # environment_setting :client_id, "TESLA_CLIENT_ID"
65
+ def environment_setting(name, variable)
66
+ define_method(name) do
67
+ # @type self: Settings
68
+ instance_variable_defined?(:"@#{name}") ? instance_variable_get(:"@#{name}") : ENV.fetch(variable, nil)
69
+ end
70
+
71
+ attr_writer(name)
72
+ end
73
+ end
74
+
75
+ # Extend whatever includes the mixin with the macros that declare the settings
76
+ #
77
+ # @api private
78
+ # @param base [Module] the module or class including {Settings}
79
+ # @return [void]
80
+ def self.included(base)
81
+ base.extend(Declaration)
82
+ end
83
+
84
+ private
85
+
86
+ # Check that a setting is a number of zero or more
87
+ #
88
+ # Any Numeric is one, since a timeout shorter than a second is a Float, and zero is one: it turns off the wait
89
+ # or the retrying the setting names rather than leaving it unset.
90
+ #
91
+ # @api private
92
+ # @param name [Symbol] the name of the setting, which the message names
93
+ # @param number [Object] the value assigned
94
+ # @return [Numeric] the number
95
+ # @raise [ArgumentError] if the value is not a number of zero or more
96
+ def validate_number(name, number)
97
+ raise ArgumentError, "Invalid #{name}: #{number.inspect}" unless number.is_a?(Numeric) && number >= 0
98
+
99
+ number
100
+ end
101
+
102
+ # Check that a setting is a whole number of times
103
+ #
104
+ # @api private
105
+ # @param name [Symbol] the name of the setting, which the message names
106
+ # @param count [Object] the value assigned
107
+ # @return [Integer] the count
108
+ # @raise [ArgumentError] if the value is not a whole number of zero or more times
109
+ def validate_count(name, count)
110
+ raise ArgumentError, "Invalid #{name}: #{count.inspect}" unless validate_number(name, count).integer?
111
+
112
+ count
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Tesla
6
+ # Checks and compares the URLs a host, a proxy, and a redirect are given as, mixed into the configuration, the
7
+ # client, the connection, and the redirect handler
8
+ #
9
+ # A URL is checked where it is assigned rather than when a request is made with it, so that the error names the
10
+ # assignment that was wrong instead of the call that happened to send the next request. The value that is rejected
11
+ # is left as it was.
12
+ #
13
+ # @api private
14
+ module URLValidation
15
+ private
16
+
17
+ # Check that a host is a URL requests can be sent to
18
+ #
19
+ # A host that carries a user and password, such as "https://user:pass@tesla.example.com", is refused: Net::HTTP
20
+ # sends neither, and the host is shown by {Client#inspect} and in error messages, which would show the
21
+ # password. The message the host is refused with leaves them out.
22
+ #
23
+ # @api private
24
+ # @param host [String] the host, including scheme
25
+ # @return [String] the host
26
+ # @raise [ArgumentError] if the host is not an HTTP or HTTPS URL, or carries a user and password
27
+ def validate_host(host)
28
+ raise ArgumentError, "Invalid host: #{redact(host.to_s)}" unless http_url?(host)
29
+ if URI(host).userinfo
30
+ raise ArgumentError, "Invalid host: #{redact(host)} carries a user and password, which are not sent"
31
+ end
32
+
33
+ host
34
+ end
35
+
36
+ # Parse and validate a proxy URL
37
+ #
38
+ # @api private
39
+ # @param proxy_url [String] the proxy URL
40
+ # @return [URI::HTTP] the proxy URI
41
+ # @raise [ArgumentError] if the proxy URL is not a valid HTTP or HTTPS URL; the message leaves out its user and
42
+ # password
43
+ def parse_proxy_uri(proxy_url)
44
+ proxy_uri = URI(proxy_url)
45
+ raise ArgumentError, "Invalid proxy URL: #{redact(proxy_url)}" unless proxy_uri.is_a?(URI::HTTP)
46
+
47
+ proxy_uri
48
+ rescue URI::InvalidURIError
49
+ raise ArgumentError, "Invalid proxy URL: #{redact(proxy_url)}"
50
+ end
51
+
52
+ # Whether a host is an HTTP or HTTPS URL with a host
53
+ # @api private
54
+ # @param host [Object] the host
55
+ # @return [Boolean] whether the host is a URL requests can be sent to
56
+ def http_url?(host)
57
+ uri = URI(host)
58
+ uri.is_a?(URI::HTTP) && !uri.host.to_s.empty?
59
+ rescue ArgumentError, URI::InvalidURIError
60
+ false
61
+ end
62
+
63
+ # Join a host and a request path, keeping any path prefix on the host
64
+ #
65
+ # A path that is a URL of its own is refused rather than followed to the host it names, since the access token
66
+ # of a request is the one issued for the host it was meant for, and a path that moved the request to another
67
+ # host would take the token with it.
68
+ #
69
+ # A path that climbs out of the prefix the host carries, such as "../.." for a host of
70
+ # "https://tesla.example.com/fleet", is refused for the same reason: the client was pointed at that prefix, and
71
+ # a path that leaves it asks the rest of the host with the access token of the client. A host without a prefix
72
+ # names the whole of its origin, so there is nothing to climb out of.
73
+ #
74
+ # @api private
75
+ # @param host [String] the host, optionally carrying a path prefix
76
+ # @param path [String] the request path
77
+ # @return [URI] the request URI
78
+ # @raise [ArgumentError] if the path is a URL of an origin other than the host, or climbs out of its prefix
79
+ def build_uri(host, path)
80
+ uri = URI.join("#{host.chomp("/")}/", path.delete_prefix("/"))
81
+ raise ArgumentError, "Path is not on #{host}: #{path}" unless on_host?(uri, host)
82
+
83
+ uri
84
+ end
85
+
86
+ # Whether two URLs share a scheme, host, and port
87
+ #
88
+ # @api private
89
+ # @param url [String, URI::Generic] one URL
90
+ # @param other [String, URI::Generic] the other URL
91
+ # @return [Boolean] whether the URLs share an origin
92
+ def same_origin?(url, other)
93
+ origin(url).eql?(origin(other))
94
+ end
95
+
96
+ # Whether a URL is the host's own, at or under the path the host names
97
+ #
98
+ # The path is compared as well as the origin, so that a URL climbing out of the prefix a host carries, such as
99
+ # "../.." for "https://tesla.example.com/fleet", is told from one the host was pointed at, as a URL of
100
+ # another scheme, host, or port is. A host that carries no prefix names the whole of its origin, so every URL
101
+ # of that origin is on it.
102
+ #
103
+ # The paths are read once the origins are known to match, so a URL that has none, such as a "mailto:" URL, is
104
+ # answered by the comparison of the origins rather than by reading the path it does not have.
105
+ #
106
+ # @api private
107
+ # @param uri [URI::Generic] the URL, as the URI it was read as
108
+ # @param host [String, URI::Generic] the host, optionally carrying a path prefix
109
+ # @return [Boolean] whether the URL is the host's own, at or under the path it names
110
+ def on_host?(uri, host)
111
+ same_origin?(uri, host) && uri.path.start_with?("#{URI(host).path.chomp("/")}/") # steep:ignore NoMethod
112
+ end
113
+
114
+ # The origin of a URL, with the scheme and host in lowercase
115
+ # @api private
116
+ # @param url [String, URI::Generic] the URL
117
+ # @return [Array] the scheme, host, and port
118
+ def origin(url)
119
+ uri = URI(url).normalize
120
+ [uri.scheme, uri.host, uri.port]
121
+ end
122
+
123
+ # Remove the user and password from a URL
124
+ # @api private
125
+ # @param url [String, nil] the URL
126
+ # @return [String, nil] the URL without its userinfo, or nil for nil
127
+ def redact(url)
128
+ url&.sub(%r{(?<=//)[^/@]*@}, "")
129
+ end
130
+ end
131
+ end
data/lib/tesla/user.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+
5
+ module Tesla
6
+ # The user of an account
7
+ # @api public
8
+ class User < Resource
9
+ inspect_with :email
10
+ identified_by :email
11
+
12
+ # @!method email
13
+ # The email address of the user
14
+ # @api public
15
+ # @return [String, nil] the email address of the user
16
+ # @example
17
+ # user.email
18
+ attribute :email
19
+
20
+ # @!method full_name
21
+ # The full name of the user
22
+ # @api public
23
+ # @return [String, nil] the full name of the user
24
+ # @example
25
+ # user.full_name
26
+ attribute :full_name
27
+
28
+ # @!method profile_image_url
29
+ # The URL of the profile image of the user
30
+ # @api public
31
+ # @return [String, nil] the URL of the profile image of the user
32
+ # @example
33
+ # user.profile_image_url
34
+ attribute :profile_image_url
35
+ end
36
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+
5
+ module Tesla
6
+ # A vehicle of the account
7
+ # @api public
8
+ class Vehicle < Resource
9
+ inspect_with :vin, :display_name, :state
10
+ identified_by :vin
11
+
12
+ # @!method id
13
+ # The ID the Fleet API knows the vehicle by
14
+ # @api public
15
+ # @return [Integer, nil] the ID the Fleet API knows the vehicle by
16
+ # @example
17
+ # vehicle.id
18
+ attribute :id
19
+
20
+ # @!method vehicle_id
21
+ # The ID the streaming and Autopark APIs know the vehicle by
22
+ # @api public
23
+ # @return [Integer, nil] the ID the streaming and Autopark APIs know the vehicle by
24
+ # @example
25
+ # vehicle.vehicle_id
26
+ attribute :vehicle_id
27
+
28
+ # @!method vin
29
+ # The vehicle identification number
30
+ # @api public
31
+ # @return [String, nil] the vehicle identification number
32
+ # @example
33
+ # vehicle.vin
34
+ attribute :vin
35
+
36
+ # @!method display_name
37
+ # The name the owner gave the vehicle
38
+ # @api public
39
+ # @return [String, nil] the name the owner gave the vehicle
40
+ # @example
41
+ # vehicle.display_name
42
+ attribute :display_name
43
+
44
+ # @!method state
45
+ # The state of the vehicle, such as "online", "asleep", or "offline"
46
+ # @api public
47
+ # @return [String, nil] the state of the vehicle
48
+ # @example
49
+ # vehicle.state
50
+ attribute :state
51
+
52
+ # @!method access_type
53
+ # The access the account has to the vehicle, such as "OWNER" or "DRIVER"
54
+ # @api public
55
+ # @return [String, nil] the access the account has to the vehicle
56
+ # @example
57
+ # vehicle.access_type
58
+ attribute :access_type
59
+
60
+ # @!method api_version
61
+ # The version of the API the vehicle speaks
62
+ # @api public
63
+ # @return [Integer, nil] the version of the API the vehicle speaks
64
+ # @example
65
+ # vehicle.api_version
66
+ attribute :api_version
67
+
68
+ # @!method in_service?
69
+ # Whether the vehicle is being serviced
70
+ # @api public
71
+ # @return [Boolean] whether the vehicle is being serviced
72
+ # @example
73
+ # vehicle.in_service?
74
+ predicate :in_service
75
+
76
+ # Whether the vehicle is online
77
+ #
78
+ # A vehicle takes commands and answers with its data only while it is online.
79
+ #
80
+ # A vehicle that is asleep is woken with {API::VehicleEndpoints#wake_up}.
81
+ #
82
+ # @api public
83
+ # @return [Boolean] whether the state of the vehicle is "online"
84
+ # @example
85
+ # vehicle.online?
86
+ def online?
87
+ state.eql?("online")
88
+ end
89
+ end
90
+ end