smartcar 3.2.0 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -1
- data/Gemfile.lock +1 -1
- data/README.md +5 -1
- data/lib/smartcar/auth_client.rb +29 -17
- data/lib/smartcar/base.rb +2 -12
- data/lib/smartcar/utils.rb +20 -0
- data/lib/smartcar/vehicle.rb +11 -8
- data/lib/smartcar/version.rb +1 -1
- data/lib/smartcar.rb +21 -16
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe729c83b0d759f2d787090e3f815ec9d97cd0b283585f8775ebe16e61b16e56
|
4
|
+
data.tar.gz: '029284bb31e8363b2cd0fe751705749e19365fb5692cbc3054325d648440117e'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e469fcb92c39256e4a779476217fdb7cd5cce276af14d61ac9a51054e5d5e485f70e8f7886fb1852d2fb9d8128edd9b85164303f7c36fdca9d919f50bb0f4cb
|
7
|
+
data.tar.gz: faf78f8b977e3548e9f44fa45a59ce3b26cc89de53e6d85f3a8bc5391ced3031bdb2801696cb237cc2bb8c801bf85f7d2dd5bb6f15ccf2904430c65549c8ac86
|
data/.rubocop.yml
CHANGED
@@ -12,7 +12,7 @@ Naming/AccessorMethodName:
|
|
12
12
|
Enabled: false
|
13
13
|
|
14
14
|
# Disabling this until we figure out a better way than using openstruct
|
15
|
-
# Currently we use open struct because this gives us an object representing the JSON
|
15
|
+
# Currently we use open struct because this gives us an object representing the JSON
|
16
16
|
# with accessor style methods.
|
17
17
|
Style/OpenStructUse:
|
18
18
|
Enabled: false
|
@@ -30,3 +30,9 @@ Metrics/MethodLength:
|
|
30
30
|
|
31
31
|
Metrics/ClassLength:
|
32
32
|
Max: 200
|
33
|
+
|
34
|
+
Metrics/CyclomaticComplexity:
|
35
|
+
Max: 10
|
36
|
+
|
37
|
+
Metrics/PerceivedComplexity:
|
38
|
+
Max: 10
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -31,6 +31,7 @@ not have access to the dashboard, please
|
|
31
31
|
|
32
32
|
- Create a new `AuthClient` object with your `client_id`, `client_secret`,
|
33
33
|
`redirect_uri`.
|
34
|
+
-
|
34
35
|
- Redirect the user to Smartcar Connect using `get_auth_url` with required `scope` or with one
|
35
36
|
of our frontend SDKs.
|
36
37
|
- The user will login, and then accept or deny your `scope`'s permissions.
|
@@ -76,6 +77,9 @@ Setup the environment variables for SMARTCAR_CLIENT_ID, SMARTCAR_CLIENT_SECRET a
|
|
76
77
|
export SMARTCAR_CLIENT_ID=<client id>
|
77
78
|
export SMARTCAR_CLIENT_SECRET=<client secret>
|
78
79
|
export SMARTCAR_REDIRECT_URI=<redirect URI>
|
80
|
+
# Optional ENV variables
|
81
|
+
export SMARTCAR_CONNECT_ORIGIN=(default_value: connect.smartcar.com): Used as the host for the URL that starts the Connect/OAuth2 flow
|
82
|
+
export SMARTCAR_AUTH_ORIGIN=(default_value: auth.smartcar.com): Used as the host for the token exchange requests
|
79
83
|
```
|
80
84
|
|
81
85
|
Example Usage for calling the reports API with oAuth token
|
@@ -101,7 +105,7 @@ Example Usage for calling the reports API with oAuth token
|
|
101
105
|
Example Usage for oAuth -
|
102
106
|
```ruby
|
103
107
|
# To get the redirect URL :
|
104
|
-
2.5.5 :002 > options = {
|
108
|
+
2.5.5 :002 > options = {mode: 'test'}
|
105
109
|
2.5.5 :003 > require 'smartcar'
|
106
110
|
2.5.5 :004 > client = Smartcar::AuthClient.new(options)
|
107
111
|
2.5.5 :005 > url = client.get_auth_url(["read_battery","read_charge","read_fuel","read_location","control_security","read_odometer","read_tires","read_vin","read_vehicle_info"], {flags: ["country:DE"]})
|
data/lib/smartcar/auth_client.rb
CHANGED
@@ -6,7 +6,7 @@ module Smartcar
|
|
6
6
|
class AuthClient
|
7
7
|
include Smartcar::Utils
|
8
8
|
|
9
|
-
attr_reader :redirect_uri, :client_id, :client_secret, :scope, :mode, :flags, :
|
9
|
+
attr_reader :redirect_uri, :client_id, :client_secret, :scope, :mode, :flags, :auth_origin, :connect_origin
|
10
10
|
|
11
11
|
# Constructor for a client object
|
12
12
|
#
|
@@ -14,15 +14,18 @@ module Smartcar
|
|
14
14
|
# @option options[:client_id] [String] - Client ID, if not passed fallsback to ENV['SMARTCAR_CLIENT_ID']
|
15
15
|
# @option options[:client_secret] [String] - Client Secret, if not passed fallsback to ENV['SMARTCAR_CLIENT_SECRET']
|
16
16
|
# @option options[:redirect_uri] [String] - Redirect URI, if not passed fallsback to ENV['SMARTCAR_REDIRECT_URI']
|
17
|
-
# @option options[:test_mode] [Boolean] -
|
18
|
-
#
|
17
|
+
# @option options[:test_mode] [Boolean] - [DEPRECATED], please use `mode` instead.
|
18
|
+
# Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/).
|
19
|
+
# @option options[:mode] [String] - Determine what mode Smartcar Connect should be launched in.
|
20
|
+
# Should be one of test, live or simulated.
|
19
21
|
# @return [Smartcar::AuthClient] Returns a Smartcar::AuthClient Object that has other methods
|
20
22
|
def initialize(options)
|
21
23
|
options[:redirect_uri] ||= get_config('SMARTCAR_REDIRECT_URI')
|
22
24
|
options[:client_id] ||= get_config('SMARTCAR_CLIENT_ID')
|
23
25
|
options[:client_secret] ||= get_config('SMARTCAR_CLIENT_SECRET')
|
24
|
-
options[:
|
25
|
-
options[:
|
26
|
+
options[:auth_origin] = ENV['SMARTCAR_AUTH_ORIGIN'] || AUTH_ORIGIN
|
27
|
+
options[:connect_origin] = ENV['SMARTCAR_CONNECT_ORIGIN'] || CONNECT_ORIGIN
|
28
|
+
options[:mode] = determine_mode(options[:test_mode], options[:mode]) || 'live'
|
26
29
|
super
|
27
30
|
end
|
28
31
|
|
@@ -55,7 +58,7 @@ module Smartcar
|
|
55
58
|
def get_auth_url(scope, options = {})
|
56
59
|
initialize_auth_parameters(scope, options)
|
57
60
|
add_single_select_options(options[:single_select])
|
58
|
-
|
61
|
+
connect_client.auth_code.authorize_url(@auth_parameters)
|
59
62
|
end
|
60
63
|
|
61
64
|
# Generates the tokens hash using the code returned in oauth process.
|
@@ -68,9 +71,9 @@ module Smartcar
|
|
68
71
|
def exchange_code(code, options = {})
|
69
72
|
set_token_url(options[:flags])
|
70
73
|
|
71
|
-
token_hash =
|
72
|
-
|
73
|
-
|
74
|
+
token_hash = auth_client.auth_code
|
75
|
+
.get_token(code, redirect_uri: redirect_uri)
|
76
|
+
.to_hash
|
74
77
|
|
75
78
|
json_to_ostruct(token_hash)
|
76
79
|
rescue OAuth2::Error => e
|
@@ -86,7 +89,7 @@ module Smartcar
|
|
86
89
|
def exchange_refresh_token(token, options = {})
|
87
90
|
set_token_url(options[:flags])
|
88
91
|
|
89
|
-
token_object = OAuth2::AccessToken.from_hash(
|
92
|
+
token_object = OAuth2::AccessToken.from_hash(auth_client, { refresh_token: token })
|
90
93
|
token_object = token_object.refresh!
|
91
94
|
|
92
95
|
json_to_ostruct(token_object.to_hash)
|
@@ -99,7 +102,7 @@ module Smartcar
|
|
99
102
|
#
|
100
103
|
# @return [Boolean]
|
101
104
|
def expired?(expires_at)
|
102
|
-
OAuth2::AccessToken.from_hash(
|
105
|
+
OAuth2::AccessToken.from_hash(auth_client, { expires_at: expires_at }).expired?
|
103
106
|
end
|
104
107
|
|
105
108
|
private
|
@@ -115,7 +118,7 @@ module Smartcar
|
|
115
118
|
params[:flags] = build_flags(flags) if flags
|
116
119
|
# Note - The inbuild interface to get the token does not allow any way to pass additional
|
117
120
|
# URL params. Hence building the token URL with the flags and setting it in client.
|
118
|
-
|
121
|
+
auth_client.options[:token_url] = auth_client.connection.build_url('/oauth/token', params).request_uri
|
119
122
|
end
|
120
123
|
|
121
124
|
def initialize_auth_parameters(scope, options)
|
@@ -142,13 +145,22 @@ module Smartcar
|
|
142
145
|
end
|
143
146
|
end
|
144
147
|
|
145
|
-
# gets the Oauth Client object
|
148
|
+
# gets the Oauth Client object configured with auth.connect.smartcar.com
|
149
|
+
#
|
150
|
+
# @return [OAuth2::Client] A Oauth Client object.
|
151
|
+
def auth_client
|
152
|
+
@auth_client ||= OAuth2::Client.new(client_id,
|
153
|
+
client_secret,
|
154
|
+
site: auth_origin)
|
155
|
+
end
|
156
|
+
|
157
|
+
# gets the Oauth Client object configured with connect.smartcar.com
|
146
158
|
#
|
147
159
|
# @return [OAuth2::Client] A Oauth Client object.
|
148
|
-
def
|
149
|
-
@
|
150
|
-
|
151
|
-
|
160
|
+
def connect_client
|
161
|
+
@connect_client ||= OAuth2::Client.new(client_id,
|
162
|
+
client_secret,
|
163
|
+
site: connect_origin)
|
152
164
|
end
|
153
165
|
end
|
154
166
|
end
|
data/lib/smartcar/base.rb
CHANGED
@@ -22,7 +22,7 @@ module Smartcar
|
|
22
22
|
# @param data [Hash] request body if needed.
|
23
23
|
#
|
24
24
|
# @return [Hash] The response Json parsed as a hash.
|
25
|
-
define_method verb do |path, data = nil, headers = {}|
|
25
|
+
define_method verb do |path, query_params = {}, data = nil, headers = {}|
|
26
26
|
response = service.send(verb) do |request|
|
27
27
|
request_headers = {}
|
28
28
|
request_headers['Authorization'] = auth_type == BASIC ? "Basic #{token}" : "Bearer #{token}"
|
@@ -32,6 +32,7 @@ module Smartcar
|
|
32
32
|
"Smartcar/#{VERSION} (#{RbConfig::CONFIG['host_os']}; #{RbConfig::CONFIG['arch']}) Ruby v#{RUBY_VERSION}"
|
33
33
|
request.headers = request_headers.merge(headers)
|
34
34
|
complete_path = "/v#{version}#{path}"
|
35
|
+
complete_path += "?#{URI.encode_www_form(query_params.compact)}" unless query_params.empty?
|
35
36
|
if verb == :get
|
36
37
|
request.url complete_path, data
|
37
38
|
else
|
@@ -46,17 +47,6 @@ module Smartcar
|
|
46
47
|
end
|
47
48
|
end
|
48
49
|
|
49
|
-
# This requires a proc 'PATH' to be defined in the class
|
50
|
-
# @param path [String] resource path
|
51
|
-
# @param query_params [Hash] query params
|
52
|
-
# @param auth [String] type of auth
|
53
|
-
#
|
54
|
-
# @return [Object]
|
55
|
-
def fetch(path:, query_params: {})
|
56
|
-
path += "?#{URI.encode_www_form(query_params)}" unless query_params.empty?
|
57
|
-
get(path)
|
58
|
-
end
|
59
|
-
|
60
50
|
private
|
61
51
|
|
62
52
|
# gets a smartcar API service/client
|
data/lib/smartcar/utils.rb
CHANGED
@@ -127,5 +127,25 @@ module Smartcar
|
|
127
127
|
|
128
128
|
path.split('/').reject(&:empty?).join('_').to_sym
|
129
129
|
end
|
130
|
+
|
131
|
+
# takes query parameters and returns them as a string
|
132
|
+
# EX - {'country': 'DE', 'flags': true} -> "county:DE flags:true"
|
133
|
+
def stringify_params(query_params)
|
134
|
+
query_params&.map { |key, value| "#{key}:#{value}" }&.join(' ')
|
135
|
+
end
|
136
|
+
|
137
|
+
def determine_mode(test_mode, mode)
|
138
|
+
unless mode.nil?
|
139
|
+
unless %w[test live simulated].include? mode
|
140
|
+
raise 'The "mode" parameter MUST be one of the following: \'test\', \'live\', \'simulated\''
|
141
|
+
end
|
142
|
+
|
143
|
+
return mode
|
144
|
+
end
|
145
|
+
return if test_mode.nil?
|
146
|
+
|
147
|
+
warn '[DEPRECATION] The "test_mode" parameter is deprecated, please use the "mode" parameter instead.'
|
148
|
+
test_mode.is_a?(TrueClass) ? 'test' : 'live'
|
149
|
+
end
|
130
150
|
end
|
131
151
|
end
|
data/lib/smartcar/vehicle.rb
CHANGED
@@ -11,6 +11,7 @@ module Smartcar
|
|
11
11
|
# @attr [Hash] options
|
12
12
|
# @attr unit_system [String] Unit system to represent the data in, defaults to Imperial
|
13
13
|
# @attr version [String] API version to be used.
|
14
|
+
# @attr flags [Hash] Object of flags where key is the name of the flag and value is string or boolean value.
|
14
15
|
# @attr service [Faraday::Connection] An optional connection object to be used for requests.
|
15
16
|
class Vehicle < Base
|
16
17
|
attr_reader :id
|
@@ -77,6 +78,7 @@ module Smartcar
|
|
77
78
|
@unit_system = options[:unit_system] || METRIC
|
78
79
|
@version = options[:version] || Smartcar.get_api_version
|
79
80
|
@service = options[:service]
|
81
|
+
@query_params = { flags: stringify_params(options[:flags]) }
|
80
82
|
|
81
83
|
raise InvalidParameterValue.new, "Invalid Units provided : #{@unit_system}" unless UNITS.include?(@unit_system)
|
82
84
|
raise InvalidParameterValue.new, 'Vehicle ID (id) is a required field' if id.nil?
|
@@ -177,11 +179,11 @@ module Smartcar
|
|
177
179
|
define_method method do
|
178
180
|
body, headers = case item[:type]
|
179
181
|
when :post
|
180
|
-
post(item[:path].call(id), item[:body])
|
182
|
+
post(item[:path].call(id), @query_params, item[:body])
|
181
183
|
when :delete
|
182
|
-
delete(item[:path].call(id))
|
184
|
+
delete(item[:path].call(id), @query_params)
|
183
185
|
else
|
184
|
-
|
186
|
+
get(item[:path].call(id), @query_params)
|
185
187
|
end
|
186
188
|
build_aliases(build_response(body, headers), item[:aliases])
|
187
189
|
end
|
@@ -195,7 +197,7 @@ module Smartcar
|
|
195
197
|
# @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-application-permissions
|
196
198
|
# and a meta attribute with the relevant items from response headers.
|
197
199
|
def permissions(paging = {})
|
198
|
-
response, headers =
|
200
|
+
response, headers = get(METHODS.dig(:permissions, :path).call(id), @query_params.merge(paging))
|
199
201
|
build_response(response, headers)
|
200
202
|
end
|
201
203
|
|
@@ -206,7 +208,7 @@ module Smartcar
|
|
206
208
|
# @return [OpenStruct] An object representing the JSON response and a meta attribute
|
207
209
|
# with the relevant items from response headers.
|
208
210
|
def subscribe!(webhook_id)
|
209
|
-
response, headers = post(METHODS.dig(:subscribe!, :path).call(id, webhook_id),
|
211
|
+
response, headers = post(METHODS.dig(:subscribe!, :path).call(id, webhook_id), @query_params)
|
210
212
|
build_aliases(build_response(response, headers), METHODS.dig(:subscribe!, :aliases))
|
211
213
|
end
|
212
214
|
|
@@ -220,7 +222,8 @@ module Smartcar
|
|
220
222
|
# swapping off the token with amt for unsubscribe.
|
221
223
|
access_token = token
|
222
224
|
self.token = amt
|
223
|
-
response, headers = delete(METHODS.dig(:unsubscribe!, :path).call(id, webhook_id)
|
225
|
+
response, headers = delete(METHODS.dig(:unsubscribe!, :path).call(id, webhook_id),
|
226
|
+
@query_params)
|
224
227
|
self.token = access_token
|
225
228
|
build_response(response, headers)
|
226
229
|
end
|
@@ -233,7 +236,7 @@ module Smartcar
|
|
233
236
|
# an OpenStruct object of the requested attribute or taises if it is an error.
|
234
237
|
def batch(paths)
|
235
238
|
request_body = { requests: paths.map { |path| { path: path } } }
|
236
|
-
response, headers = post("/vehicles/#{id}/batch", request_body)
|
239
|
+
response, headers = post("/vehicles/#{id}/batch", @query_params, request_body)
|
237
240
|
process_batch_response(response, headers)
|
238
241
|
end
|
239
242
|
|
@@ -249,7 +252,7 @@ module Smartcar
|
|
249
252
|
# response body and a "meta" attribute with the relevant items from response headers.
|
250
253
|
def request(method, path, body = {}, headers = {})
|
251
254
|
path = "/vehicles/#{id}/#{path}"
|
252
|
-
raw_response, headers = send(method.downcase, path, body, headers)
|
255
|
+
raw_response, headers = send(method.downcase, path, @query_params, body, headers)
|
253
256
|
meta = build_meta(headers)
|
254
257
|
json_to_ostruct({ body: raw_response, meta: meta })
|
255
258
|
end
|
data/lib/smartcar/version.rb
CHANGED
data/lib/smartcar.rb
CHANGED
@@ -22,7 +22,8 @@ module Smartcar
|
|
22
22
|
}.freeze
|
23
23
|
|
24
24
|
# Path for smartcar oauth
|
25
|
-
|
25
|
+
CONNECT_ORIGIN = 'https://connect.smartcar.com'
|
26
|
+
AUTH_ORIGIN = 'https://auth.smartcar.com'
|
26
27
|
%w[success code test live force auto metric imperial].each do |constant|
|
27
28
|
# Constant to represent the value
|
28
29
|
const_set(constant.upcase, constant.freeze)
|
@@ -38,6 +39,7 @@ module Smartcar
|
|
38
39
|
@api_version = '2.0'
|
39
40
|
|
40
41
|
class << self
|
42
|
+
include Smartcar::Utils
|
41
43
|
# Module method Used to set api version to be used.
|
42
44
|
# This method can be used at the top to set the version and any
|
43
45
|
# following request will use the version set here unless overridden
|
@@ -67,7 +69,10 @@ module Smartcar
|
|
67
69
|
# @option options [String] :client_secret Client Secret that overrides ENV
|
68
70
|
# @option options [String] :version API version to use, defaults to what is globally set
|
69
71
|
# @option options [Hash] :flags A hash of flag name string as key and a string or boolean value.
|
70
|
-
# @option options
|
72
|
+
# @option options[Boolean] :test_mode [DEPRECATED], please use `mode` instead.
|
73
|
+
# Launch Smartcar Connect in test mode(https://smartcar.com/docs/guides/testing/).
|
74
|
+
# @option options [String] :mode Determine what mode Smartcar Connect should be launched in.
|
75
|
+
# Should be one of test, live or simulated.
|
71
76
|
# @option options [String] :test_mode_compatibility_level this is required argument while using
|
72
77
|
# test mode with a real vin. For more information refer to docs.
|
73
78
|
# @option options [Faraday::Connection] :service Optional connection object to be used for requests
|
@@ -88,9 +93,9 @@ module Smartcar
|
|
88
93
|
|
89
94
|
base_object.token = generate_basic_auth(options, base_object)
|
90
95
|
|
91
|
-
base_object.build_response(*base_object.
|
92
|
-
|
93
|
-
|
96
|
+
base_object.build_response(*base_object.get(
|
97
|
+
PATHS[:compatibility],
|
98
|
+
build_compatibility_params(vin, scope, country, options)
|
94
99
|
))
|
95
100
|
end
|
96
101
|
|
@@ -112,7 +117,7 @@ module Smartcar
|
|
112
117
|
service: options[:service]
|
113
118
|
}
|
114
119
|
)
|
115
|
-
base_object.build_response(*base_object.
|
120
|
+
base_object.build_response(*base_object.get(PATHS[:user]))
|
116
121
|
end
|
117
122
|
|
118
123
|
# Module method Returns a paged list of all vehicles connected to the application for the current authorized user.
|
@@ -134,9 +139,9 @@ module Smartcar
|
|
134
139
|
service: options[:service]
|
135
140
|
}
|
136
141
|
)
|
137
|
-
base_object.build_response(*base_object.
|
138
|
-
|
139
|
-
|
142
|
+
base_object.build_response(*base_object.get(
|
143
|
+
PATHS[:vehicles],
|
144
|
+
paging
|
140
145
|
))
|
141
146
|
end
|
142
147
|
|
@@ -169,15 +174,15 @@ module Smartcar
|
|
169
174
|
scope: scope.join(' '),
|
170
175
|
country: country
|
171
176
|
}
|
172
|
-
query_params[:flags] = options[:flags]
|
173
|
-
query_params[:mode] = options[:test_mode].is_a?(TrueClass) ? 'test' : 'live' unless options[:test_mode].nil?
|
177
|
+
query_params[:flags] = stringify_params(options[:flags])
|
174
178
|
|
175
|
-
|
176
|
-
query_params[:test_mode_compatibility_level] =
|
177
|
-
options[:test_mode_compatibility_level]
|
178
|
-
query_params[:mode] = 'test'
|
179
|
-
end
|
179
|
+
mode = determine_mode(options[:test_mode], options[:mode])
|
180
180
|
|
181
|
+
unless options[:test_mode_compatibility_level].nil?
|
182
|
+
query_params[:test_mode_compatibility_level] = options[:test_mode_compatibility_level]
|
183
|
+
mode = 'test'
|
184
|
+
end
|
185
|
+
query_params[:mode] = mode unless mode.nil?
|
181
186
|
query_params
|
182
187
|
end
|
183
188
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smartcar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ashwin Subramanian
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -235,7 +235,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
235
235
|
- !ruby/object:Gem::Version
|
236
236
|
version: '0'
|
237
237
|
requirements: []
|
238
|
-
rubygems_version: 3.
|
238
|
+
rubygems_version: 3.1.6
|
239
239
|
signing_key:
|
240
240
|
specification_version: 4
|
241
241
|
summary: Ruby Gem to access smartcar APIs (https://smartcar.com/docs/)
|