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,403 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "deep_copy"
4
+ require_relative "errors/invalid_response"
5
+
6
+ module Tesla
7
+ # Base class for objects that wrap Tesla Fleet API responses
8
+ #
9
+ # The endpoints answer with the resources the library defines, which declare their readers with {.attribute},
10
+ # {.predicate}, {.time_attribute}, {.resource_attribute}, and {.resource_list_attribute}, and declare what they
11
+ # inspect with and what identifies them with {.inspect_with} and {.identified_by}. Those declarations are private,
12
+ # since no endpoint answers with a resource of your own, so they can change within 0.x; the readers they declare,
13
+ # {.attribute_names}, and the rest of the instance interface are public.
14
+ #
15
+ # @api public
16
+ class Resource
17
+ include DeepCopy
18
+
19
+ # The raw attributes from the API response
20
+ # @api public
21
+ # @return [Hash{String => Object}] the raw attributes
22
+ # @example Get the raw attributes
23
+ # vehicle.attributes["vin"]
24
+ attr_reader :attributes
25
+
26
+ # Build a list of resources from a list of attribute hashes
27
+ #
28
+ # @api public
29
+ # @param list [Array<Hash{String, Symbol => Object}>] the attribute hashes
30
+ # @return [Array<Resource>] the resources
31
+ # @example Build a list of vehicles
32
+ # Tesla::Vehicle.list(JSON.parse(body).fetch("response"))
33
+ def self.list(list)
34
+ list.map { |attributes| new(attributes) } #: Array[instance]
35
+ end
36
+
37
+ # The macros that declare the readers of a resource, extended into {Resource}
38
+ # @api private
39
+ module Declaration
40
+ # Define a reader for an attribute
41
+ #
42
+ # Endpoints spell some fields differently, so a reader may declare several keys and reads the first
43
+ # one the response contains.
44
+ #
45
+ # @api private
46
+ # @param name [Symbol] the name of the reader
47
+ # @param keys [Array<String, Symbol>] the attribute keys, most preferred first (defaults to the name)
48
+ # @return [Symbol] the name of the reader
49
+ # @example Declare a reader
50
+ # attribute :name
51
+ def attribute(name, *keys)
52
+ keys = keys_for(name, keys)
53
+ define_method(name) do
54
+ # @type self: Resource
55
+ value_of(keys)
56
+ end
57
+ record_attribute(name)
58
+ end
59
+
60
+ # Define a predicate for a boolean attribute
61
+ #
62
+ # @api private
63
+ # @param name [Symbol] the name of the attribute (the reader is suffixed with a question mark)
64
+ # @param keys [Array<String, Symbol>] the attribute keys, most preferred first (defaults to the name)
65
+ # @return [Symbol] the name of the reader
66
+ # @example Declare a predicate for a boolean field
67
+ # predicate :in_service
68
+ def predicate(name, *keys)
69
+ keys = keys_for(name, keys)
70
+ define_method(:"#{name}?") do
71
+ # @type self: Resource
72
+ !!value_of(keys)
73
+ end
74
+ record_attribute(:"#{name}?")
75
+ end
76
+
77
+ # Define a reader that reads a timestamp attribute as a Time
78
+ #
79
+ # The Fleet API writes a timestamp as the milliseconds since the Unix epoch. The reader raises {InvalidResponse}
80
+ # when the attribute is not a number of them.
81
+ #
82
+ # @api private
83
+ # @param name [Symbol] the name of the reader
84
+ # @param keys [Array<String, Symbol>] the attribute keys, most preferred first (defaults to the name)
85
+ # @return [Symbol] the name of the reader
86
+ # @example Declare a reader that parses a timestamp field
87
+ # time_attribute :timestamp
88
+ def time_attribute(name, *keys)
89
+ keys = keys_for(name, keys)
90
+ define_method(name) do
91
+ # @type self: Resource
92
+ value = value_of(keys)
93
+ value && parse_time(value)
94
+ end
95
+ record_attribute(name)
96
+ end
97
+
98
+ # Define a reader that wraps a nested object in a resource
99
+ #
100
+ # @api private
101
+ # @param name [Symbol] the name of the reader
102
+ # @param resource_class [Class<Resource>] the resource the nested object is wrapped in
103
+ # @param keys [Array<String, Symbol>] the attribute keys, most preferred first (defaults to the name)
104
+ # @return [Symbol] the name of the reader
105
+ # @example Declare a reader that wraps a nested object
106
+ # resource_attribute :charge_state, ChargeState
107
+ def resource_attribute(name, resource_class, *keys)
108
+ keys = keys_for(name, keys)
109
+ define_method(name) do
110
+ # @type self: Resource
111
+ value = value_of(keys)
112
+ value && resource_class.new(value)
113
+ end
114
+ record_attribute(name)
115
+ end
116
+
117
+ # Define a reader that wraps each of a list of nested objects in a resource
118
+ #
119
+ # @api private
120
+ # @param name [Symbol] the name of the reader
121
+ # @param resource_class [Class<Resource>] the resource each nested object is wrapped in
122
+ # @param keys [Array<String, Symbol>] the attribute keys, most preferred first (defaults to the name)
123
+ # @return [Symbol] the name of the reader
124
+ # @example Declare a reader that wraps a list of nested objects
125
+ # resource_list_attribute :superchargers, ChargingSite
126
+ def resource_list_attribute(name, resource_class, *keys)
127
+ keys = keys_for(name, keys)
128
+ define_method(name) do
129
+ # @type self: Resource
130
+ resource_class.list(value_of(keys) || [])
131
+ end
132
+ record_attribute(name)
133
+ end
134
+
135
+ # The names of the readers the class declares
136
+ #
137
+ # These are the readers declared with attribute, predicate, time_attribute, resource_attribute, and
138
+ # resource_list_attribute, and a pattern matches a resource by these names (see {#deconstruct_keys}).
139
+ #
140
+ # A subclass of a resource inherits the readers its superclass declared, until it declares readers of its own.
141
+ #
142
+ # @api public
143
+ # @return [Array<Symbol>] the reader names, in the order they were declared
144
+ # @example
145
+ # Tesla::User.attribute_names # => [:email, :full_name, :profile_image_url]
146
+ def attribute_names
147
+ @attribute_names || (superclass.attribute_names if superclass.respond_to?(:attribute_names)) || [] # steep:ignore NoMethod
148
+ end
149
+
150
+ private
151
+
152
+ # Record a declared reader for {.attribute_names}
153
+ #
154
+ # @api private
155
+ # @param name [Symbol] the name of the reader
156
+ # @return [Symbol] the name of the reader
157
+ def record_attribute(name)
158
+ @attribute_names = [*attribute_names, name]
159
+ name
160
+ end
161
+
162
+ # The attribute keys a reader reads
163
+ #
164
+ # @api private
165
+ # @param name [Symbol] the name of the reader
166
+ # @param keys [Array<String, Symbol>] the declared keys
167
+ # @return [Array<String>] the keys, most preferred first
168
+ def keys_for(name, keys)
169
+ (keys.empty? ? [name] : keys).map(&:to_s)
170
+ end
171
+ end
172
+
173
+ extend Declaration
174
+
175
+ # Declare which readers appear in the inspect output
176
+ #
177
+ # @api private
178
+ # @param readers [Array<Symbol>] the readers to show
179
+ # @return [Array<Symbol>] the readers to show
180
+ # @example Declare the readers a resource inspects with
181
+ # inspect_with :vin, :display_name
182
+ def self.inspect_with(*readers)
183
+ @inspect_readers = readers
184
+ end
185
+
186
+ # The readers shown in the inspect output
187
+ #
188
+ # @api private
189
+ # @return [Array<Symbol>] the readers to show, inherited from the superclass until the class declares its own
190
+ # @example
191
+ # Tesla::Vehicle.inspect_readers # => [:vin, :display_name, :state]
192
+ def self.inspect_readers
193
+ @inspect_readers || (superclass.inspect_readers if superclass.respond_to?(:inspect_readers)) || [] # steep:ignore NoMethod
194
+ end
195
+
196
+ # Declare which readers identify the resource
197
+ #
198
+ # Resources with an identity compare equal when those readers match, even if other attributes differ.
199
+ #
200
+ # @api private
201
+ # @param readers [Array<Symbol>] the identifying readers
202
+ # @return [Array<Symbol>] the identifying readers
203
+ # @example Declare what identifies a resource
204
+ # identified_by :vin
205
+ def self.identified_by(*readers)
206
+ @identity_readers = readers
207
+ end
208
+
209
+ # The readers that identify the resource
210
+ #
211
+ # @api private
212
+ # @return [Array<Symbol>] the identifying readers, inherited from the superclass until the class declares its
213
+ # own, and empty when the resource is identified by all of its attributes
214
+ # @example
215
+ # Tesla::Vehicle.identity_readers # => [:vin]
216
+ def self.identity_readers
217
+ @identity_readers || (superclass.identity_readers if superclass.respond_to?(:identity_readers)) || [] # steep:ignore NoMethod
218
+ end
219
+
220
+ # Initialize a new resource
221
+ #
222
+ # The attributes are deeply copied and frozen, so resources are immutable values and the hash passed in
223
+ # stays mutable. Symbol keys are converted to strings, at every level.
224
+ #
225
+ # @api public
226
+ # @param attributes [Hash{String, Symbol => Object}] the raw attributes from the API response
227
+ # @return [Resource] a new instance
228
+ # @example Wrap a parsed response
229
+ # Tesla::Vehicle.new(JSON.parse(body).fetch("response"))
230
+ def initialize(attributes)
231
+ @attributes = deep_freeze(attributes)
232
+ end
233
+
234
+ # Read a raw attribute
235
+ #
236
+ # @api public
237
+ # @param key [String, Symbol] the attribute key
238
+ # @return [Object, nil] the attribute value
239
+ # @example Read an attribute that has no reader
240
+ # vehicle[:tokens]
241
+ def [](key)
242
+ attributes[key.to_s]
243
+ end
244
+
245
+ # Convert the resource to a hash
246
+ #
247
+ # The hash is a copy the caller owns and may change, as the hash `to_h` answers with elsewhere in Ruby is, and
248
+ # so is everything nested in it: the hashes, arrays, and strings a resource holds are frozen, so they are copied
249
+ # rather than handed to the caller to raise `FrozenError` on. {#attributes} answers with the frozen hash itself,
250
+ # for reading it without the copy.
251
+ #
252
+ # @api public
253
+ # @return [Hash{String => Object}] a copy of the raw attributes
254
+ # @example Convert a vehicle to a hash
255
+ # vehicle.to_h
256
+ # @example Add a field of your own to the copy
257
+ # vehicle.to_h.merge!("fetched_at" => Time.now)
258
+ # @example Change a field nested in the copy
259
+ # data.to_h["charge_state"]["fetched_at"] = Time.now
260
+ def to_h
261
+ deep_dup(attributes) #: Hash[String, untyped]
262
+ end
263
+
264
+ # The attributes a pattern asks for, read by the declared readers
265
+ #
266
+ # A resource matches a `case`/`in` pattern by the names in {.attribute_names}, read as the readers read them, so
267
+ # a pattern sees a timestamp as a `Time` and a boolean as a predicate such as `in_service?`.
268
+ #
269
+ # A name whose reader raises {InvalidResponse}, such as a timestamp that is not a number, is left out rather than
270
+ # raised from: a pattern asking for it does not match, and one asking for the rest of the attributes, such as
271
+ # `in {vin:, **rest}`, matches without it. Reading that reader still raises, so a caller that asks for the
272
+ # attribute is told why it cannot be read.
273
+ #
274
+ # @api public
275
+ # @param keys [Array<Symbol>, nil] the names the pattern asks for, or nil for all of them
276
+ # @return [Hash{Symbol => Object}] the requested attributes, without the ones that cannot be read
277
+ # @example Match a vehicle by its state
278
+ # case Tesla.vehicle("5YJSA11111111111")
279
+ # in {state: "online", display_name:} then display_name
280
+ # end
281
+ def deconstruct_keys(keys)
282
+ names = self.class.attribute_names
283
+ names &= keys unless keys.nil?
284
+ requested = {} #: Hash[Symbol, untyped]
285
+ names.each do |name|
286
+ requested[name] = public_send(name)
287
+ rescue InvalidResponse
288
+ # The attribute cannot be read, so the pattern is answered without it
289
+ end
290
+ requested
291
+ end
292
+
293
+ # Compare with another resource
294
+ #
295
+ # @api public
296
+ # @param other [Object] the object to compare with
297
+ # @return [Boolean] true if the other object is the same kind of resource with the same identity
298
+ # @example Compare two vehicles
299
+ # Tesla.vehicle("5YJSA11111111111") == Tesla.vehicle("5YJSA11111111111") # => true
300
+ def ==(other)
301
+ other.instance_of?(self.class) && identity == other.identity
302
+ end
303
+
304
+ # Compare with another resource for use as a hash key
305
+ #
306
+ # Unlike {#==}, identity values are compared with `eql?`, so this agrees with {#hash}.
307
+ #
308
+ # @api public
309
+ # @param other [Object] the object to compare with
310
+ # @return [Boolean] true if the other object is the same kind of resource with an eql? identity
311
+ # @example Compare two vehicles strictly
312
+ # Tesla.vehicle("5YJSA11111111111").eql?(Tesla.vehicle("5YJSA11111111111")) # => true
313
+ def eql?(other)
314
+ other.instance_of?(self.class) && identity.eql?(other.identity)
315
+ end
316
+
317
+ # Summarize the resource for the console
318
+ #
319
+ # Only the readers declared with {.inspect_with} are shown, so the output stays short and never includes secrets.
320
+ #
321
+ # @api public
322
+ # @return [String] the summary
323
+ # @example Inspect a vehicle
324
+ # vehicle.inspect # => #<Tesla::Vehicle vin="5YJSA11111111111" display_name="Nikola 2.0" state="online">
325
+ def inspect
326
+ fields = inspect_values.map { |reader, value| " #{reader}=#{value.inspect}" }
327
+ "#<#{self.class}#{fields.join}>"
328
+ end
329
+
330
+ # Summarize the resource as a String
331
+ #
332
+ # The summary {#inspect} answers with, so that a resource written into a message or a log reads as the one the
333
+ # console shows rather than as the address the object sits at.
334
+ #
335
+ # @api public
336
+ # @return [String] the summary
337
+ # @example Write a vehicle into a message
338
+ # "woke #{vehicle}" # => 'woke #<Tesla::Vehicle vin="5YJSA11111111111" display_name="Nikola 2.0" state="online">'
339
+ def to_s = inspect
340
+
341
+ # Generate a hash code for the resource
342
+ #
343
+ # @api public
344
+ # @return [Integer] the hash code
345
+ # @example Use resources as hash keys
346
+ # {vehicle => true}
347
+ def hash
348
+ [self.class, identity].hash
349
+ end
350
+
351
+ protected
352
+
353
+ # The values that identify the resource
354
+ #
355
+ # A resource answers with these to the resource it is compared with, rather than to a caller, which reads the
356
+ # readers {.identified_by} names or the attributes themselves: the values are what the comparison is made of,
357
+ # and are an Array of readers for one resource and the whole of the attributes for another.
358
+ #
359
+ # @api private
360
+ # @return [Array<Object>, Hash{String => Object}] the identifying values, or all attributes when no identity is
361
+ # declared
362
+ # @example Compare two vehicles by the VIN that identifies them
363
+ # Tesla.vehicle("5YJSA11111111111") == Tesla.vehicle("5YJSA11111111111") # => true
364
+ def identity
365
+ readers = self.class.identity_readers
366
+ readers.empty? ? attributes : readers.map { |reader| public_send(reader) }
367
+ end
368
+
369
+ private
370
+
371
+ # The readers {#inspect} shows, with their values
372
+ #
373
+ # These are the readers {.inspect_with} declares, which a resource that shows more when it has it adds to.
374
+ #
375
+ # @api private
376
+ # @return [Hash{Symbol => Object}] the value of each reader shown, in the order they are shown
377
+ def inspect_values = self.class.inspect_readers.to_h { |reader| [reader, public_send(reader)] }
378
+
379
+ # The value of the first of the given keys the response contains
380
+ # @api private
381
+ # @param keys [Array<String>] the attribute keys, most preferred first
382
+ # @return [Object, nil] the value, or nil when the response contains none of the keys
383
+ def value_of(keys)
384
+ keys.each { |key| return self[key] if attributes.key?(key) }
385
+ nil
386
+ end
387
+
388
+ # Read a timestamp attribute as a Time in UTC
389
+ #
390
+ # The attribute is the milliseconds since the Unix epoch, which are read as a Rational number of seconds so that
391
+ # none of them are lost to a Float.
392
+ #
393
+ # @api private
394
+ # @param value [Object] the attribute value
395
+ # @return [Time] the time
396
+ # @raise [InvalidResponse] if the value is not a timestamp
397
+ def parse_time(value)
398
+ Time.at(Rational(value, 1000), in: "UTC")
399
+ rescue ArgumentError, TypeError
400
+ raise InvalidResponse.new(body: value.to_s, message: "#{value.inspect} is not a timestamp")
401
+ end
402
+ end
403
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require_relative "errors/bad_gateway"
5
+ require_relative "errors/bad_request"
6
+ require_relative "errors/client_error"
7
+ require_relative "errors/device_unexpected_response"
8
+ require_relative "errors/forbidden"
9
+ require_relative "errors/gateway_timeout"
10
+ require_relative "errors/http_error"
11
+ require_relative "errors/internal_server_error"
12
+ require_relative "errors/locked"
13
+ require_relative "errors/method_not_allowed"
14
+ require_relative "errors/misdirected_request"
15
+ require_relative "errors/not_acceptable"
16
+ require_relative "errors/not_found"
17
+ require_relative "errors/payment_required"
18
+ require_relative "errors/precondition_failed"
19
+ require_relative "errors/request_timeout"
20
+ require_relative "errors/server_error"
21
+ require_relative "errors/service_unavailable"
22
+ require_relative "errors/too_many_requests"
23
+ require_relative "errors/unauthorized"
24
+ require_relative "errors/unavailable_for_legal_reasons"
25
+ require_relative "errors/unprocessable_content"
26
+
27
+ module Tesla
28
+ # Parses HTTP responses from the Tesla Fleet API
29
+ # @api private
30
+ class ResponseParser
31
+ # Mapping of HTTP status codes to error classes
32
+ ERROR_MAP = {
33
+ 400 => BadRequest,
34
+ 401 => Unauthorized,
35
+ 402 => PaymentRequired,
36
+ 403 => Forbidden,
37
+ 404 => NotFound,
38
+ 405 => MethodNotAllowed,
39
+ 406 => NotAcceptable,
40
+ 408 => RequestTimeout,
41
+ 412 => PreconditionFailed,
42
+ 421 => MisdirectedRequest,
43
+ 422 => UnprocessableContent,
44
+ 423 => Locked,
45
+ 429 => TooManyRequests,
46
+ 451 => UnavailableForLegalReasons,
47
+ 500 => InternalServerError,
48
+ 502 => BadGateway,
49
+ 503 => ServiceUnavailable,
50
+ 504 => GatewayTimeout,
51
+ 540 => DeviceUnexpectedResponse
52
+ }.freeze
53
+
54
+ # The error classes for the 4xx and 5xx statuses ERROR_MAP does not name
55
+ STATUS_CLASS_ERRORS = {4 => ClientError, 5 => ServerError}.freeze
56
+ private_constant :ERROR_MAP, :STATUS_CLASS_ERRORS
57
+
58
+ # Parse an HTTP response
59
+ #
60
+ # @api private
61
+ # @param response [Net::HTTPResponse] the HTTP response to parse
62
+ # @return [String] the response body
63
+ # @raise [HTTPError] if the response is not successful
64
+ # @example Parse a response
65
+ # parser.parse(response: response)
66
+ def parse(response:)
67
+ raise error(response) unless response.is_a?(Net::HTTPSuccess)
68
+
69
+ response.body.to_s
70
+ end
71
+
72
+ private
73
+
74
+ # Create an error from a response
75
+ # @api private
76
+ # @param response [Net::HTTPResponse] the HTTP response
77
+ # @return [HTTPError] the error
78
+ def error(response)
79
+ error_class(response).new(response:)
80
+ end
81
+
82
+ # Get the error class for a response
83
+ # @api private
84
+ # @param response [Net::HTTPResponse] the HTTP response
85
+ # @return [Class] the error class: the one named for the status, else ClientError or ServerError for its class of
86
+ # status, else HTTPError
87
+ def error_class(response)
88
+ status = Integer(response.code)
89
+ ERROR_MAP.fetch(status) { STATUS_CLASS_ERRORS.fetch(status / 100, HTTPError) }
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Tesla
6
+ # Reads the `Retry-After` header of a response, for {HTTPError#retry_after} and for {RetryHandler}
7
+ #
8
+ # The Fleet API sends the header with a 429 Too Many Requests response, as a number of seconds or an HTTP date.
9
+ #
10
+ # @api private
11
+ module RetryAfter
12
+ # A `Retry-After` header that is a number of seconds, which HTTP writes in decimal digits alone
13
+ DELTA_SECONDS = /\A-?\d+\z/
14
+ private_constant :DELTA_SECONDS
15
+
16
+ private
17
+
18
+ # The seconds a response asks to wait before the request is sent again
19
+ #
20
+ # @api private
21
+ # @param response [Net::HTTPResponse] the HTTP response
22
+ # @return [Integer, nil] the seconds to wait, rounded up and never negative, or nil when the response has no
23
+ # `Retry-After` header or one that is neither a number of seconds nor an HTTP date
24
+ def retry_after_of(response)
25
+ value = response["Retry-After"]
26
+ return if value.nil?
27
+
28
+ seconds = DELTA_SECONDS.match?(value) ? value.to_i : (Time.httpdate(value) - Time.now).ceil
29
+ [seconds, 0].max
30
+ rescue ArgumentError
31
+ nil
32
+ end
33
+ end
34
+ end