smartcar 2.4.0 → 3.0.2

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.
@@ -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