smartcar 2.4.0 → 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smartcar
4
+ # AuthClient class to take care of the Oauth 2.0 with Smartcar APIs
5
+ #
6
+ class AuthClient
7
+ include Smartcar::Utils
8
+
9
+ attr_reader :redirect_uri, :client_id, :client_secret, :scope, :mode, :flags, :origin
10
+
11
+ # Constructor for a client object
12
+ #
13
+ # @param [Hash] options
14
+ # @option options[:client_id] [String] - Client ID, if not passed fallsback to ENV['SMARTCAR_CLIENT_ID']
15
+ # @option options[:client_secret] [String] - Client Secret, if not passed fallsback to ENV['SMARTCAR_CLIENT_SECRET']
16
+ # @option options[:redirect_uri] [String] - Redirect URI, if not passed fallsback to ENV['SMARTCAR_REDIRECT_URI']
17
+ # @option options[:test_mode] [Boolean] - Setting this to 'true' runs it in test mode.
18
+ #
19
+ # @return [Smartcar::AuthClient] Returns a Smartcar::AuthClient Object that has other methods
20
+ def initialize(options)
21
+ options[:redirect_uri] ||= get_config('SMARTCAR_REDIRECT_URI')
22
+ options[:client_id] ||= get_config('SMARTCAR_CLIENT_ID')
23
+ options[:client_secret] ||= get_config('SMARTCAR_CLIENT_SECRET')
24
+ options[:mode] = options[:test_mode].is_a?(TrueClass) ? TEST : LIVE
25
+ options[:origin] = ENV['SMARTCAR_AUTH_ORIGIN'] || AUTH_ORIGIN
26
+ super
27
+ end
28
+
29
+ # Generate the OAuth authorization URL.
30
+ # @param scope [Array<String>] Array of permissions that specify what the user can access
31
+ # EXAMPLE : ['read_odometer', 'read_vehicle_info', 'required:read_location']
32
+ # For further details refer to https://smartcar.com/docs/guides/scope/
33
+ # @param [Hash] options
34
+ # @option options[:force_prompt] [Boolean] - Setting `force_prompt` to
35
+ # `true` will show the permissions approval screen on every authentication
36
+ # attempt, even if the user has previously consented to the exact scope of
37
+ # permissions.
38
+ # @option options[:single_select] [Hash] - An optional object that sets the
39
+ # behavior of the grant dialog displayed to the user. Object can contain two keys :
40
+ # - enabled - Boolean value, if set to `true`,
41
+ # `single_select` limits the user to selecting only one vehicle.
42
+ # - vin - String vin, if set, Smartcar will only authorize the vehicle
43
+ # with the specified VIN.
44
+ # See the [Single Select guide](https://smartcar.com/docs/guides/single-select/) for more information.
45
+ # @option options[:state] [String] - OAuth state parameter passed to the
46
+ # redirect uri. This parameter may be used for identifying the user who
47
+ # initiated the request.
48
+ # @option options[:make_bypass] [String] - `make_bypass' is an optional parameter that allows
49
+ # users to bypass the car brand selection screen.
50
+ # For a complete list of supported makes, please see our
51
+ # [API Reference](https://smartcar.com/docs/api#authorization) documentation.
52
+ # @option options[:flags] [Hash] - A hash of flag name string as key and a string or boolean value.
53
+ #
54
+ # @return [String] Authorization URL string
55
+ def get_auth_url(scope, options = {})
56
+ initialize_auth_parameters(scope, options)
57
+ add_single_select_options(options[:single_select])
58
+ client.auth_code.authorize_url(@auth_parameters)
59
+ end
60
+
61
+ # Generates the tokens hash using the code returned in oauth process.
62
+ # @param code [String] This is the code that is returned after user
63
+ # visits and authorizes on the authorization URL.
64
+ # @param [Hash] options
65
+ # @option options[:flags] [Hash] - A hash of flag name string as key and a string or boolean value.
66
+ #
67
+ # @return [Hash] Hash of token, refresh token, expiry info and token type
68
+ def exchange_code(code, options = {})
69
+ set_token_url(options[:flags])
70
+
71
+ token_hash = client.auth_code
72
+ .get_token(code, redirect_uri: redirect_uri)
73
+ .to_hash
74
+
75
+ json_to_ostruct(token_hash)
76
+ rescue OAuth2::Error => e
77
+ raise build_error(e.response.status, e.response.body, e.response.headers)
78
+ end
79
+
80
+ # Refreshing the access token
81
+ # @param token [String] refresh_token received during token exchange
82
+ # @param [Hash] options
83
+ # @option options[:flags] [Hash] - A hash of flag name string as key and a string or boolean value.
84
+ #
85
+ # @return [Hash] Hash of token, refresh token, expiry info and token type
86
+ def exchange_refresh_token(token, options = {})
87
+ set_token_url(options[:flags])
88
+
89
+ token_object = OAuth2::AccessToken.from_hash(client, { refresh_token: token })
90
+ token_object = token_object.refresh!
91
+
92
+ json_to_ostruct(token_object.to_hash)
93
+ rescue OAuth2::Error => e
94
+ raise build_error(e.response.status, e.response.body, e.response.headers)
95
+ end
96
+
97
+ # Checks if token is expired using Oauth2 classes
98
+ # @param expires_at [Number] expires_at as time since epoch
99
+ #
100
+ # @return [Boolean]
101
+ def expired?(expires_at)
102
+ OAuth2::AccessToken.from_hash(client, { expires_at: expires_at }).expired?
103
+ end
104
+
105
+ private
106
+
107
+ def build_flags(flags)
108
+ return unless flags
109
+
110
+ flags.map { |key, value| "#{key}:#{value}" }.join(' ')
111
+ end
112
+
113
+ def set_token_url(flags)
114
+ params = {}
115
+ params[:flags] = build_flags(flags) if flags
116
+ # Note - The inbuild interface to get the token does not allow any way to pass additional
117
+ # URL params. Hence building the token URL with the flags and setting it in client.
118
+ client.options[:token_url] = client.connection.build_url('/oauth/token', params).request_uri
119
+ end
120
+
121
+ def initialize_auth_parameters(scope, options)
122
+ @auth_parameters = {
123
+ response_type: CODE,
124
+ redirect_uri: redirect_uri,
125
+ mode: mode,
126
+ scope: scope.join(' ')
127
+ }
128
+ @auth_parameters[:approval_prompt] = options[:force_prompt] ? FORCE : AUTO unless options[:force_prompt].nil?
129
+ @auth_parameters[:state] = options[:state] if options[:state]
130
+ @auth_parameters[:make] = options[:make_bypass] if options[:make_bypass]
131
+ @auth_parameters[:flags] = build_flags(options[:flags]) if options[:flags]
132
+ end
133
+
134
+ def add_single_select_options(single_select)
135
+ return unless single_select
136
+
137
+ if single_select[:vin]
138
+ @auth_parameters[:single_select_vin] = single_select[:vin]
139
+ @auth_parameters[:single_select] = true
140
+ elsif !single_select[:enabled].nil?
141
+ @auth_parameters[:single_select] = single_select[:enabled]
142
+ end
143
+ end
144
+
145
+ # gets the Oauth Client object
146
+ #
147
+ # @return [OAuth2::Client] A Oauth Client object.
148
+ def client
149
+ @client ||= OAuth2::Client.new(client_id,
150
+ client_secret,
151
+ site: origin)
152
+ end
153
+ end
154
+ end
data/lib/smartcar/base.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'oauth2'
2
4
  require 'base64'
3
5
  module Smartcar
@@ -8,67 +10,57 @@ module Smartcar
8
10
 
9
11
  # Error raised when an invalid parameter is passed.
10
12
  class InvalidParameterValue < StandardError; end
11
- # Constant for Bearer auth type
12
- BEARER = 'BEARER'.freeze
13
13
  # Constant for Basic auth type
14
- BASIC = 'BASIC'.freeze
14
+ BASIC = 'Basic'
15
15
  # Number of seconds to wait for response
16
16
  REQUEST_TIMEOUT = 310
17
17
 
18
- attr_accessor :token, :error, :meta, :unit_system, :version
18
+ attr_accessor :token, :error, :unit_system, :version, :auth_type
19
19
 
20
- %i{get post patch put delete}.each do |verb|
20
+ %i[get post patch put delete].each do |verb|
21
21
  # meta programming and define all Restful methods.
22
22
  # @param path [String] the path to hit for the request.
23
23
  # @param data [Hash] request body if needed.
24
24
  #
25
25
  # @return [Hash] The response Json parsed as a hash.
26
- define_method verb do |path, data=nil|
26
+ define_method verb do |path, data = nil|
27
27
  response = service.send(verb) do |request|
28
- request.headers['Authorization'] = "BEARER #{token}"
29
- request.headers['Authorization'] = "BASIC #{get_basic_auth}" if data[:auth] == BASIC
28
+ request.headers['Authorization'] = auth_type == BASIC ? "Basic #{token}" : "Bearer #{token}"
30
29
  request.headers['sc-unit-system'] = unit_system if unit_system
31
- request.headers['Content-Type'] = "application/json"
30
+ request.headers['Content-Type'] = 'application/json'
32
31
  complete_path = "/v#{version}#{path}"
33
- if verb==:get
32
+ if verb == :get
34
33
  request.url complete_path, data
35
34
  else
36
35
  request.url complete_path
37
36
  request.body = data.to_json if data
38
37
  end
39
38
  end
40
- error = get_error(response)
41
- raise error if error
42
- [JSON.parse(response.body), response.headers]
39
+ handle_error(response)
40
+ # required to handle unsubscribe response
41
+ body = response.body.empty? ? '{}' : response.body
42
+ [JSON.parse(body), response.headers]
43
43
  end
44
44
  end
45
45
 
46
46
  # This requires a proc 'PATH' to be defined in the class
47
47
  # @param path [String] resource path
48
- # @param options [Hash] query params
48
+ # @param query_params [Hash] query params
49
49
  # @param auth [String] type of auth
50
50
  #
51
51
  # @return [Object]
52
- def fetch(path: , options: {}, auth: 'BEARER')
53
- _path = path
54
- _path += "?#{URI.encode_www_form(options)}" unless options.empty?
55
- get(_path, {auth: auth})
52
+ def fetch(path:, query_params: {})
53
+ path += "?#{URI.encode_www_form(query_params)}" unless query_params.empty?
54
+ get(path)
56
55
  end
57
56
 
58
57
  private
59
58
 
60
- # returns auth token for BASIC auth
61
- #
62
- # @return [String] Base64 encoding of CLIENT:SECRET
63
- def get_basic_auth
64
- Base64.strict_encode64("#{get_config('CLIENT_ID')}:#{get_config('CLIENT_SECRET')}")
65
- end
66
-
67
59
  # gets a smartcar API service/client
68
60
  #
69
61
  # @return [OAuth2::AccessToken] An initialized AccessToken instance that acts as service client
70
62
  def service
71
- @service ||= Faraday.new(url: SITE, request: { timeout: REQUEST_TIMEOUT })
63
+ @service ||= Faraday.new(url: ENV['SMARTCAR_API_ORIGIN'] || API_ORIGIN, request: { timeout: REQUEST_TIMEOUT })
72
64
  end
73
65
  end
74
66
  end
@@ -1,5 +1,7 @@
1
- # Utils module , provides utility methods to underlying classes
1
+ # frozen_string_literal: true
2
+
2
3
  module Smartcar
4
+ # Utils module , provides utility methods to underlying classes
3
5
  module Utils
4
6
  # A constructor to take a hash and assign it to the instance variables
5
7
  # @param options = {} [Hash] Could by any class's hash, but the first level keys should be defined in the class
@@ -11,36 +13,116 @@ module Smartcar
11
13
  end
12
14
  end
13
15
 
14
- # Utility method to return a hash of the isntance variables
15
- #
16
- # @return [Hash] hash of all instance variables
17
- def to_hash
18
- instance_variables.each_with_object({}) do |attribute, hash|
19
- hash[attribute.to_s.delete("@").to_sym] = instance_variable_get(attribute)
20
- end
21
- end
22
-
23
16
  # gets a given env variable, checks for existence and throws exception if not present
24
17
  # @param config_name [String] key of the env variable
25
18
  #
26
19
  # @return [String] value of the env variable
27
20
  def get_config(config_name)
28
- config_name = "INTEGRATION_#{config_name}" if ENV['MODE'] == 'test'
21
+ # ENV.MODE is set to test by e2e tests.
22
+ config_name = "E2E_#{config_name}" if ENV['MODE'] == 'test'
29
23
  raise Smartcar::ConfigNotFound, "Environment variable #{config_name} not found !" unless ENV[config_name]
24
+
30
25
  ENV[config_name]
31
26
  end
32
27
 
33
- # Given the response from smartcar API, returns an error object if needed
34
- # @param response [Object] response Object with status and body
28
+ # Converts a hash to RecursiveOpenStruct (a powered up OpenStruct object).
29
+ # NOTE - Do not replace with the more elegant looking
30
+ # JSON.parse(meta_hash.to_json, object_class: OpenStruct)
31
+ # this is because we had an app using OJ as their json parser which led to an issue using the
32
+ # above mentioned method. Source : https://github.com/ohler55/oj/issues/239
33
+ # @param hash [Hash] json object as hash
35
34
  #
36
- # @return [Object] nil OR Error object
37
- def get_error(response)
35
+ # @return [RecursiveOpenStruct]
36
+ def json_to_ostruct(hash)
37
+ RecursiveOpenStruct.new(hash, recurse_over_arrays: true)
38
+ end
39
+
40
+ def build_meta(headers)
41
+ meta_hash = {
42
+ 'sc-data-age' => :data_age,
43
+ 'sc-unit-system' => :unit_system,
44
+ 'sc-request-id' => :request_id
45
+ }.each_with_object({}) do |(header_name, key), meta|
46
+ meta[key] = headers[header_name] if headers[header_name]
47
+ end
48
+ meta = json_to_ostruct(meta_hash)
49
+ meta.data_age &&= DateTime.parse(meta.data_age)
50
+
51
+ meta
52
+ end
53
+
54
+ def build_response(body, headers)
55
+ response = json_to_ostruct(body)
56
+ response.meta = build_meta(headers)
57
+ response
58
+ end
59
+
60
+ def build_aliases(response, aliases)
61
+ (aliases || []).each do |original_name, alias_name|
62
+ response.send("#{alias_name}=".to_sym, response.send(original_name.to_sym))
63
+ end
64
+
65
+ response
66
+ end
67
+
68
+ def build_error(status, body_string, headers)
69
+ content_type = headers['content-type'] || ''
70
+ return SmartcarError.new(status, body_string, headers) unless content_type.include?('application/json')
71
+
72
+ begin
73
+ parsed_body = JSON.parse(body_string, { symbolize_names: true })
74
+ rescue StandardError => e
75
+ return SmartcarError.new(
76
+ status,
77
+ {
78
+ message: e.message,
79
+ type: 'SDK_ERROR'
80
+ },
81
+ headers
82
+ )
83
+ end
84
+
85
+ return SmartcarError.new(status, parsed_body, headers) if parsed_body[:error] || parsed_body[:type]
86
+
87
+ SmartcarError.new(status, parsed_body.merge({ type: 'SDK_ERROR' }), headers)
88
+ end
89
+
90
+ # Given the response from smartcar API, throws an error if needed
91
+ # @param response [Object] response Object with status and body
92
+ def handle_error(response)
38
93
  status = response.status
39
- return nil if [200,204].include?(status)
40
- return Smartcar::ServiceUnavailableError.new("Service Unavailable - #{response.body}") if status == 404
41
- return Smartcar::BadRequestError.new("Bad Request - #{response.body}") if status == 400
42
- return Smartcar::AuthenticationError.new("Authentication error") if status == 401
43
- return Smartcar::ExternalServiceError.new("API error - #{response.body}")
94
+ return nil if [200, 204].include?(status)
95
+
96
+ raise build_error(response.status, response.body, response.headers)
97
+ end
98
+
99
+ def process_batch_response(response_body, response_headers)
100
+ response_object = OpenStruct.new
101
+ response_body['responses'].each do |item|
102
+ attribute_name = convert_path_to_attribute(item['path'])
103
+ aliases = Vehicle::METHODS[attribute_name.to_sym][:aliases]
104
+ # merging the top level request headers and separate headers for each item of batch
105
+ headers = response_headers.merge(item['headers'] || {})
106
+ response = if [200, 204].include?(item['code'])
107
+ build_aliases(build_response(item['body'], headers), aliases)
108
+ else
109
+ build_error(item['code'], item['body'].to_json, headers)
110
+ end
111
+ response_object.define_singleton_method attribute_name do
112
+ raise response if response.is_a?(SmartcarError)
113
+
114
+ response
115
+ end
116
+ end
117
+ response_object
118
+ end
119
+
120
+ # takes a path and converts it to the keys we use.
121
+ # EX - '/charge' -> :charge, '/battery/capacity' -> :battery_capacity
122
+ def convert_path_to_attribute(path)
123
+ return :attributes if path == '/'
124
+
125
+ path.split('/').reject(&:empty?).join('_').to_sym
44
126
  end
45
127
  end
46
128
  end
@@ -1,304 +1,238 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Smartcar
2
4
  # Vehicle class to connect to vehicle basic info,disconnect, lock unlock and get all vehicles API
3
5
  # For ease of use, this also has methods define to be able to call other resources on a vehicle object
4
6
  # For Ex. Vehicle object will be treate as an entity and doing vehicle_object.
5
7
  # Battery should return Battery object.
6
8
  #
7
- #@attr [String] token Access token used to connect to Smartcar API.
8
- #@attr [String] id Smartcar vehicle ID.
9
- #@attr [String] unit_system unit system to represent the data in.
9
+ # @attr [String] token Access token used to connect to Smartcar API.
10
+ # @attr [String] id Smartcar vehicle ID.
11
+ # @attr [Hash] options
12
+ # @attr unit_system [String] Unit system to represent the data in, defaults to Imperial
13
+ # @attr version [String] API version to be used.
10
14
  class Vehicle < Base
11
- # Path for hitting compatibility end point
12
- COMPATIBLITY_PATH = '/compatibility'.freeze
13
-
14
- # Path for hitting vehicle ids end point
15
- PATH = Proc.new{|id| "/vehicles/#{id}"}
16
-
17
15
  attr_reader :id
18
16
 
19
- def initialize(token:, id:, unit_system: IMPERIAL, version: Smartcar.get_api_version)
20
- raise InvalidParameterValue.new, "Invalid Units provided : #{unit_system}" unless UNITS.include?(unit_system)
21
- raise InvalidParameterValue.new, "Vehicle ID (id) is a required field" if id.nil?
22
- raise InvalidParameterValue.new, "Access Token(token) is a required field" if token.nil?
17
+ # @private
18
+ METHODS = {
19
+ permissions: { path: proc { |id| "/vehicles/#{id}/permissions" }, skip: true },
20
+ attributes: { path: proc { |id| "/vehicles/#{id}" } },
21
+ battery: {
22
+ path: proc { |id| "/vehicles/#{id}/battery" },
23
+ aliases: { 'percentRemaining' => 'percentage_remaining' }
24
+ },
25
+ battery_capacity: { path: proc { |id| "/vehicles/#{id}/battery/capacity" } },
26
+ charge: {
27
+ path: proc { |id| "/vehicles/#{id}/charge" },
28
+ aliases: { 'isPluggedIn' => 'is_plugged_in?' }
29
+ },
30
+ engine_oil: {
31
+ path: proc { |id| "/vehicles/#{id}/engine/oil" },
32
+ aliases: { 'lifeRemaining' => 'life_remaining' }
33
+ },
34
+ fuel: {
35
+ path: proc { |id| "/vehicles/#{id}/fuel" },
36
+ aliases: {
37
+ 'amountRemaining' => 'amount_remaining',
38
+ 'percentRemaining' => 'percent_remaining'
39
+ }
40
+ },
41
+ location: { path: proc { |id| "/vehicles/#{id}/location" } },
42
+ odometer: { path: proc { |id| "/vehicles/#{id}/odometer" } },
43
+ tire_pressure: {
44
+ path: proc { |id| "/vehicles/#{id}/tires/pressure" },
45
+ aliases: {
46
+ 'backLeft' => 'back_left',
47
+ 'backRight' => 'back_right',
48
+ 'frontLeft' => 'front_left',
49
+ 'frontRight' => 'front_right'
50
+ }
51
+ },
52
+ vin: { path: proc { |id| "/vehicles/#{id}/vin" } },
53
+ disconnect!: { type: :delete, path: proc { |id| "/vehicles/#{id}/application" } },
54
+ lock!: { type: :post, path: proc { |id| "/vehicles/#{id}/security" }, body: { action: 'LOCK' } },
55
+ unlock!: { type: :post, path: proc { |id| "/vehicles/#{id}/security" }, body: { action: 'UNLOCK' } },
56
+ start_charge!: { type: :post, path: proc { |id| "/vehicles/#{id}/charge" }, body: { action: 'START' } },
57
+ stop_charge!: { type: :post, path: proc { |id| "/vehicles/#{id}/charge" }, body: { action: 'STOP' } },
58
+ subscribe!: {
59
+ type: :post,
60
+ path: proc { |id, webhook_id| "/vehicles/#{id}/webhooks/#{webhook_id}" },
61
+ aliases: {
62
+ 'webhookId' => 'webhook_id',
63
+ 'vehicleId' => 'vehicle_id'
64
+ },
65
+ skip: true
66
+ },
67
+ unsubscribe!: { type: :post, path: proc { |id, webhook_id|
68
+ "/vehicles/#{id}/webhooks/#{webhook_id}"
69
+ }, skip: true }
70
+ }.freeze
71
+
72
+ def initialize(token:, id:, options: { unit_system: METRIC, version: Smartcar.get_api_version })
73
+ super
23
74
  @token = token
24
75
  @id = id
25
- @unit_system = unit_system
26
- @version = version
27
- end
28
-
29
- # Class method Used to get all the vehicles in the app. This only returns
30
- # API - https://smartcar.com/docs/api#get-all-vehicles
31
- # @param token [String] - Access token
32
- # @param options [Hash] - Optional filter parameters (check documentation)
33
- #
34
- # @return [Array] of vehicle IDs(Strings)
35
- def self.all_vehicle_ids(token:, options: {}, version: Smartcar.get_api_version)
36
- response, meta = new(token: token, id: 'none', version: version).fetch(
37
- path: PATH.call(''),
38
- options: options
39
- )
40
- response['vehicles']
41
- end
42
-
43
- # Class method Used to check compatiblity for VIN and scope
44
- # API - https://smartcar.com/docs/api#connect-compatibility
45
- # @param vin [String] VIN of the vehicle to be checked
46
- # @param scope [Array of Strings] - array of scopes
47
- # @param country [String] An optional country code according to
48
- # [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2).
49
- # Defaults to US.
50
- #
51
- # @return [Boolean] true or false
52
- def self.compatible?(vin:, scope:, country: 'US', version: Smartcar.get_api_version)
53
- raise InvalidParameterValue.new, "vin is a required field" if vin.nil?
54
- raise InvalidParameterValue.new, "scope is a required field" if scope.nil?
55
-
56
- response, meta = new(token: 'none', id: 'none', version: version).fetch(path: COMPATIBLITY_PATH,
57
- options: {
58
- vin: vin,
59
- scope: scope.join(' '),
60
- country: country
61
- },
62
- auth: BASIC
63
- )
64
- response['compatible']
65
- end
66
-
67
- # Method to get batch requests
68
- # API - https://smartcar.com/docs/api#post-batch-request
69
- # @param attributes [Array] Array of strings or symbols of attributes to be fetched together
70
- #
71
- # @return [Hash] Hash wth key as requested attribute(symbol) and value as Error OR Object of the requested attribute
72
- def batch(attributes = [])
73
- raise InvalidParameterValue.new, "vin is a required field" if attributes.nil?
74
- request_body = get_batch_request_body(attributes)
75
- response, _meta = post(PATH.call(id) + "/batch", request_body)
76
- process_batch_response(response)
77
- end
78
-
79
- # Fetch the list of permissions that this application has been granted for
80
- # this vehicle
81
- # EX : Smartcar::Vehicle.new(token: token, id: id).permissions
82
- # @param options [Hash] - Optional filter parameters (check documentation)
83
- #
84
- # @return [Permissions] object
85
- def permissions(options: {})
86
- get_attribute(Permissions)
87
- end
76
+ @unit_system = options[:unit_system]
77
+ @version = options[:version]
88
78
 
89
- # Method Used toRevoke access for the current requesting application
90
- # API - https://smartcar.com/docs/api#delete-disconnect
91
- #
92
- # @return [Boolean] true if success
93
- def disconnect!
94
- response = delete(PATH.call(id) + "/application")
95
- response['status'] == SUCCESS
79
+ raise InvalidParameterValue.new, "Invalid Units provided : #{@unit_system}" unless UNITS.include?(@unit_system)
80
+ raise InvalidParameterValue.new, 'Vehicle ID (id) is a required field' if id.nil?
81
+ raise InvalidParameterValue.new, 'Access Token(token) is a required field' if token.nil?
96
82
  end
97
83
 
98
- # Methods Used to lock car
99
- # API - https://smartcar.com/docs/api#post-security
84
+ # @!method attributes()
85
+ # Returns make model year and id of the vehicle
100
86
  #
101
- # @return [Boolean] true if success
102
- def lock!
103
- lock_or_unlock!(action: Smartcar::LOCK)
104
- end
105
-
106
- # Methods Used to unlock car
107
- # API - https://smartcar.com/docs/api#post-security
87
+ # API Documentation - https://smartcar.com/api#get-vehicle-attributes
108
88
  #
109
- # @return [Boolean] true if success
110
- def unlock!
111
- lock_or_unlock!(action: Smartcar::UNLOCK)
112
- end
89
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/api#get-vehicle-attributes
90
+ # and a meta attribute with the relevant items from response headers.
113
91
 
114
- # Method used to start charging a car
92
+ # @!method battery()
93
+ # Returns the state of charge (SOC) and remaining range of an electric or plug-in hybrid vehicle's battery.
115
94
  #
95
+ # API Documentation https://smartcar.com/docs/api#get-ev-battery
116
96
  #
117
- # @return [Boolean] true if success
118
- def start_charge!
119
- start_or_stop_charge!(action: Smartcar::START_CHARGE)
120
- end
97
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-ev-battery
98
+ # and a meta attribute with the relevant items from response headers.
121
99
 
122
- # Method used to stop charging a car
100
+ # @!method battery_capacity()
101
+ # Returns the capacity of an electric or plug-in hybrid vehicle's battery.
123
102
  #
103
+ # API Documentation https://smartcar.com/docs/api#get-ev-battery-capacity
124
104
  #
125
- # @return [Boolean] true if success
126
- def stop_charge!
127
- start_or_stop_charge!(action: Smartcar::STOP_CHARGE)
128
- end
129
-
130
- # Returns make model year and id of the vehicle
131
- # API - https://smartcar.com/api#get-vehicle-attributes
132
- #
133
- # @return [VehicleAttributes] object
134
- def vehicle_attributes
135
- get_attribute(VehicleAttributes)
136
- end
137
-
138
- # Returns the state of charge (SOC) and remaining range of an electric or
139
- # plug-in hybrid vehicle's battery.
140
- # API - https://smartcar.com/docs/api#get-ev-battery
141
- #
142
- # @return [Battery] object
143
- def battery
144
- get_attribute(Battery)
145
- end
146
-
147
- # Returns the capacity of an electric or
148
- # plug-in hybrid vehicle's battery.
149
- # API - https://smartcar.com/docs/api#get-ev-battery-capacity
150
- #
151
- # @return [Battery] object
152
- def battery_capacity
153
- get_attribute(BatteryCapacity)
154
- end
105
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-ev-battery-capacity
106
+ # and a meta attribute with the relevant items from response headers.
155
107
 
108
+ # @!method charge()
156
109
  # Returns the current charge status of the vehicle.
157
- # API - https://smartcar.com/docs/api#get-ev-battery
158
110
  #
159
- # @return [Charge] object
160
- def charge
161
- get_attribute(Charge)
162
- end
111
+ # API Documentation https://smartcar.com/docs/api#get-ev-battery
112
+ #
113
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-ev-battery
114
+ # and a meta attribute with the relevant items from response headers.
163
115
 
116
+ # @!method engine_oil()
164
117
  # Returns the remaining life span of a vehicle's engine oil
165
- # API - https://smartcar.com/docs/api#get-engine-oil-life
166
118
  #
167
- # @return [EngineOil] object
168
- def engine_oil
169
- get_attribute(EngineOil)
170
- end
119
+ # API Documentation https://smartcar.com/docs/api#get-engine-oil-life
120
+ #
121
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-engine-oil-life
122
+ # and a meta attribute with the relevant items from response headers.
171
123
 
124
+ # @!method fuel()
172
125
  # Returns the status of the fuel remaining in the vehicle's gas tank.
173
- # API - https://smartcar.com/docs/api#get-fuel-tank
174
126
  #
175
- # @return [Fuel] object
176
- def fuel
177
- get_attribute(Fuel)
178
- end
127
+ # API Documentation https://smartcar.com/docs/api#get-fuel-tank
128
+ #
129
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-fuel-tank
130
+ # and a meta attribute with the relevant items from response headers.
179
131
 
132
+ # @!method location()
180
133
  # Returns the last known location of the vehicle in geographic coordinates.
181
- # API - https://smartcar.com/docs/api#get-location
182
134
  #
183
- # @return [Location] object
184
- def location
185
- get_attribute(Location)
186
- end
135
+ # API Documentation https://smartcar.com/docs/api#get-location
136
+ #
137
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-location
138
+ # and a meta attribute with the relevant items from response headers.
187
139
 
140
+ # @!method odometer()
188
141
  # Returns the vehicle's last known odometer reading.
189
- # API - https://smartcar.com/docs/api#get-odometer
190
142
  #
191
- # @return [Odometer] object
192
- def odometer
193
- get_attribute(Odometer)
194
- end
143
+ # API Documentation https://smartcar.com/docs/api#get-odometer
144
+ #
145
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-odometer
146
+ # and a meta attribute with the relevant items from response headers.
195
147
 
148
+ # @!method tire_pressure()
196
149
  # Returns the air pressure of each of the vehicle's tires.
197
- # API - https://smartcar.com/docs/api#get-tire-pressure
198
150
  #
199
- # @return [TirePressure] object
200
- def tire_pressure
201
- get_attribute(TirePressure)
202
- end
151
+ # API Documentation https://smartcar.com/docs/api#get-tire-pressure
152
+ #
153
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-tire-pressure
154
+ # and a meta attribute with the relevant items from response headers.
203
155
 
156
+ # @!method vin()
204
157
  # Returns the vehicle's manufacturer identifier (VIN).
205
- # API - https://smartcar.com/docs/api#get-vin
206
158
  #
207
- # @return [String] Vin of the vehicle.
208
- def vin
209
- _object = get_attribute(Vin)
210
- @vin ||= _object.vin
211
- end
212
-
213
- private
214
-
215
- def allowed_attributes
216
- @allowed_attributes ||= {
217
- battery: get_path(Battery),
218
- charge: get_path(Charge),
219
- engine_oil: get_path(EngineOil),
220
- fuel: get_path(Fuel),
221
- location: get_path(Location),
222
- odometer: get_path(Odometer),
223
- permissions: get_path(Permissions),
224
- tire_pressure: get_path(TirePressure),
225
- vin: get_path(Vin),
226
- }
227
- end
228
-
229
- def path_to_class
230
- @path_to_class ||= {
231
- get_path(Battery) => Battery,
232
- get_path(Charge) => Charge,
233
- get_path(EngineOil) => EngineOil,
234
- get_path(Fuel) => Fuel,
235
- get_path(Location) => Location,
236
- get_path(Odometer) => Odometer,
237
- get_path(Permissions) => Permissions,
238
- get_path(TirePressure) => TirePressure,
239
- get_path(Vin) => Vin,
240
- }
241
- end
242
-
243
- # @private
244
- BatchItemResponse = Struct.new(:body, :status, :headers) do
245
- def body_with_meta
246
- body.merge(meta: headers)
247
- end
248
- end
249
-
250
- def get_batch_request_body(attributes)
251
- attributes = validated_attributes(attributes)
252
- requests = attributes.each_with_object([]) do |item, requests|
253
- requests << { path: allowed_attributes[item] }
254
- end
255
- { requests: requests }
256
- end
257
-
258
- def process_batch_response(responses)
259
- inverted_map = allowed_attributes.invert
260
- responses["responses"].each_with_object({}) do |response, result|
261
- item_response = BatchItemResponse.new(response["body"], response["code"], response["headers"])
262
- error = get_error(item_response)
263
- path = response["path"]
264
- result[inverted_map[path]] = error || get_object(path_to_class[path], item_response.body_with_meta)
265
- end
266
- end
267
-
268
- def validated_attributes(attributes)
269
- attributes.map!(&:to_sym)
270
- unsupported_attributes = (attributes - allowed_attributes.keys) || []
271
- unless unsupported_attributes.empty?
272
- message = "Unsupported attribute(s) requested in batch - #{unsupported_attributes.join(',')}"
273
- raise InvalidParameterValue.new, message
159
+ # API Documentation https://smartcar.com/docs/api#get-vin
160
+ #
161
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-vin
162
+ # and a meta attribute with the relevant items from response headers.
163
+
164
+ # NOTES :
165
+ # - We only generate the methods where there is no query string or additional options considering thats
166
+ # the majority, for all the ones that require parameters, write them separately.
167
+ # Ex. permissions, subscribe, unsubscribe
168
+ # - The following snippet generates methods dynamically , but if we are adding a new item,
169
+ # make sure we also add the doc for it.
170
+ METHODS.each do |method, item|
171
+ # We add these to the METHODS object to keep it in one place, but mark them to be skipped
172
+ # for dynamic generation
173
+ next if item[:skip]
174
+
175
+ define_method method do
176
+ body, headers = case item[:type]
177
+ when :post
178
+ post(item[:path].call(id), item[:body])
179
+ when :delete
180
+ delete(item[:path].call(id))
181
+ else
182
+ fetch(path: item[:path].call(id))
183
+ end
184
+ build_aliases(build_response(body, headers), item[:aliases])
274
185
  end
275
- attributes
276
186
  end
277
187
 
278
- def get_attribute(klass)
279
- body, meta = fetch(
280
- path: klass::PATH.call(id)
281
- )
282
- get_object(klass, body.merge(meta: meta))
283
- end
284
-
285
- def get_object(klass, data)
286
- klass.new(data)
188
+ # Method to fetch the list of permissions that this application has been granted for this vehicle.
189
+ # API - https://smartcar.com/docs/api#get-application-permissions
190
+ #
191
+ # @param paging [Hash] Optional filter parameters (check documentation)
192
+ #
193
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-application-permissions
194
+ # and a meta attribute with the relevant items from response headers.
195
+ def permissions(paging = {})
196
+ response, headers = fetch(path: METHODS.dig(:permissions, :path).call(id), query_params: paging)
197
+ build_response(response, headers)
287
198
  end
288
199
 
289
- def get_path(klass)
290
- path = klass::PATH.call(id)
291
- path.split("/vehicles/#{id}").last
200
+ # Subscribe the vehicle to given webhook Id.
201
+ #
202
+ # @param webhook_id [String] Webhook id to subscribe to
203
+ #
204
+ # @return [OpenStruct] And object representing the JSON response and a meta attribute
205
+ # with the relevant items from response headers.
206
+ def subscribe!(webhook_id)
207
+ response, headers = post(METHODS.dig(:subscribe!, :path).call(id, webhook_id), {})
208
+ build_aliases(build_response(response, headers), METHODS.dig(:subscribe!, :aliases))
292
209
  end
293
210
 
294
- def lock_or_unlock!(action:)
295
- response, meta = post(PATH.call(id) + "/security", { action: action })
296
- response['status'] == SUCCESS
211
+ # Unubscribe the vehicle from given webhook Id.
212
+ #
213
+ # @param amt [String] Application management token
214
+ # @param webhook_id [String] Webhook id to subscribe to
215
+ #
216
+ # @return [OpenStruct] Meta attribute with the relevant items from response headers.
217
+ def unsubscribe!(amt, webhook_id)
218
+ # swapping off the token with amt for unsubscribe.
219
+ access_token = token
220
+ self.token = amt
221
+ response, headers = delete(METHODS.dig(:unsubscribe!, :path).call(id, webhook_id))
222
+ self.token = access_token
223
+ build_response(response, headers)
297
224
  end
298
225
 
299
- def start_or_stop_charge!(action:)
300
- response, meta = post(PATH.call(id) + "/charge", { action: action })
301
- response['status'] == SUCCESS
226
+ # Method to get batch requests.
227
+ # API - https://smartcar.com/docs/api#post-batch-request
228
+ # @param paths [Array] Array of paths as strings. Ex ['/battery', '/odometer']
229
+ #
230
+ # @return [OpenStruct] Object with one attribute per requested path that returns
231
+ # an OpenStruct object of the requested attribute or taises if it is an error.
232
+ def batch(paths)
233
+ request_body = { requests: paths.map { |path| { path: path } } }
234
+ response, headers = post("/vehicles/#{id}/batch", request_body)
235
+ process_batch_response(response, headers)
302
236
  end
303
237
  end
304
238
  end