smartcar 2.1.0 → 3.0.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.
@@ -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.parse(token_hash.to_json, object_class: OpenStruct)
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.parse(token_object.to_hash.to_json, object_class: OpenStruct)
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,74 +1,66 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'oauth2'
2
4
  require 'base64'
3
5
  module Smartcar
4
6
  # The Base class for all of the other class.
5
7
  # Let other classes inherit from here and put common methods here.
6
8
  class Base
7
- include Utils
9
+ include Smartcar::Utils
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
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
30
- request.headers['sc-unit-system'] = unit_system
31
- request.headers['Content-Type'] = "application/json"
32
- complete_path = "/#{API_VERSION}#{path}"
33
- if verb==:get
28
+ request.headers['Authorization'] = auth_type == BASIC ? "Basic #{token}" : "Bearer #{token}"
29
+ request.headers['sc-unit-system'] = unit_system if unit_system
30
+ request.headers['Content-Type'] = 'application/json'
31
+ complete_path = "/v#{version}#{path}"
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,44 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smartcar
1
4
  # Utils module , provides utility methods to underlying classes
2
5
  module Utils
3
- # A constructor to take a hash and assign it to the instance variables
4
- # @param options = {} [Hash] Could by any class's hash, but the first level keys should be defined in the class
5
- #
6
- # @return [Subclass os Base] Returns object of any subclass like Report
7
- def initialize(options = {})
8
- options.each do |attribute, value|
9
- instance_variable_set("@#{attribute}", value)
6
+ # A constructor to take a hash and assign it to the instance variables
7
+ # @param options = {} [Hash] Could by any class's hash, but the first level keys should be defined in the class
8
+ #
9
+ # @return [Subclass os Base] Returns object of any subclass like Report
10
+ def initialize(options = {})
11
+ options.each do |attribute, value|
12
+ instance_variable_set("@#{attribute}", value)
13
+ end
10
14
  end
11
- end
12
15
 
13
- # Utility method to return a hash of the isntance variables
14
- #
15
- # @return [Hash] hash of all instance variables
16
- def to_hash
17
- instance_variables.each_with_object({}) do |attribute, hash|
18
- hash[attribute.to_s.delete("@").to_sym] = instance_variable_get(attribute)
16
+ # gets a given env variable, checks for existence and throws exception if not present
17
+ # @param config_name [String] key of the env variable
18
+ #
19
+ # @return [String] value of the env variable
20
+ def get_config(config_name)
21
+ # ENV.MODE is set to test by e2e tests.
22
+ config_name = "E2E_#{config_name}" if ENV['MODE'] == 'test'
23
+ raise Smartcar::ConfigNotFound, "Environment variable #{config_name} not found !" unless ENV[config_name]
24
+
25
+ ENV[config_name]
19
26
  end
20
- end
21
27
 
22
- # gets a given env variable, checks for existence and throws exception if not present
23
- # @param config_name [String] key of the env variable
24
- #
25
- # @return [String] value of the env variable
26
- def get_config(config_name)
27
- config_name = "INTEGRATION_#{config_name}" if ENV['MODE'] == 'test'
28
- raise Smartcar::ConfigNotFound, "Environment variable #{config_name} not found !" unless ENV[config_name]
29
- ENV[config_name]
30
- end
28
+ def build_meta(headers)
29
+ meta_hash = {
30
+ 'sc-data-age' => :data_age,
31
+ 'sc-unit-system' => :unit_system,
32
+ 'sc-request-id' => :request_id
33
+ }.each_with_object({}) do |(header_name, key), meta|
34
+ meta[key] = headers[header_name] if headers[header_name]
35
+ end
36
+ meta = JSON.parse(meta_hash.to_json, object_class: OpenStruct)
37
+ meta.data_age &&= DateTime.parse(meta.data_age)
38
+
39
+ meta
40
+ end
41
+
42
+ def build_response(body, headers)
43
+ response = JSON.parse(body.to_json, object_class: OpenStruct)
44
+ response.meta = build_meta(headers)
45
+ response
46
+ end
47
+
48
+ def build_aliases(response, aliases)
49
+ (aliases || []).each do |original_name, alias_name|
50
+ response.send("#{alias_name}=".to_sym, response.send(original_name.to_sym))
51
+ end
52
+
53
+ response
54
+ end
55
+
56
+ def build_error(status, body_string, headers)
57
+ content_type = headers['content-type'] || ''
58
+ return SmartcarError.new(status, body_string, headers) unless content_type.include?('application/json')
31
59
 
32
- # Given the response from smartcar API, returns an error object if needed
33
- # @param response [Object] response Object with status and body
34
- #
35
- # @return [Object] nil OR Error object
36
- def get_error(response)
37
- status = response.status
38
- return nil if [200,204].include?(status)
39
- return Smartcar::ServiceUnavailableError.new("Service Unavailable - #{response.body}") if status == 404
40
- return Smartcar::BadRequestError.new("Bad Request - #{response.body}") if status == 400
41
- return Smartcar::AuthenticationError.new("Authentication error") if status == 401
42
- return Smartcar::ExternalServiceError.new("API error - #{response.body}")
60
+ begin
61
+ parsed_body = JSON.parse(body_string, { symbolize_names: true })
62
+ rescue StandardError => e
63
+ return SmartcarError.new(
64
+ status,
65
+ {
66
+ message: e.message,
67
+ type: 'SDK_ERROR'
68
+ },
69
+ headers
70
+ )
71
+ end
72
+
73
+ return SmartcarError.new(status, parsed_body, headers) if parsed_body[:error] || parsed_body[:type]
74
+
75
+ SmartcarError.new(status, parsed_body.merge({ type: 'SDK_ERROR' }), headers)
76
+ end
77
+
78
+ # Given the response from smartcar API, throws an error if needed
79
+ # @param response [Object] response Object with status and body
80
+ def handle_error(response)
81
+ status = response.status
82
+ return nil if [200, 204].include?(status)
83
+
84
+ raise build_error(response.status, response.body, response.headers)
85
+ end
86
+
87
+ def process_batch_response(response_body, response_headers)
88
+ response_object = OpenStruct.new
89
+ response_body['responses'].each do |item|
90
+ attribute_name = convert_path_to_attribute(item['path'])
91
+ aliases = Vehicle::METHODS[attribute_name.to_sym][:aliases]
92
+ # merging the top level request headers and separate headers for each item of batch
93
+ headers = response_headers.merge(item['headers'])
94
+ response = if [200, 204].include?(item['code'])
95
+ build_aliases(build_response(item['body'], headers), aliases)
96
+ else
97
+ build_error(item['code'], item['body'].to_json, headers)
98
+ end
99
+ response_object.define_singleton_method attribute_name do
100
+ raise response if response.is_a?(SmartcarError)
101
+
102
+ response
103
+ end
104
+ end
105
+ response_object
106
+ end
107
+
108
+ # takes a path and converts it to the keys we use.
109
+ # EX - '/charge' -> :charge, '/battery/capacity' -> :battery_capacity
110
+ def convert_path_to_attribute(path)
111
+ return :attributes if path == '/'
112
+
113
+ path.split('/').reject(&:empty?).join('_').to_sym
114
+ end
43
115
  end
44
116
  end
@@ -1,307 +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
- include Utils
12
-
13
-
14
- # Path for hitting compatibility end point
15
- COMPATIBLITY_PATH = '/compatibility'.freeze
16
-
17
- # Path for hitting vehicle ids end point
18
- PATH = Proc.new{|id| "/vehicles/#{id}"}
19
-
20
15
  attr_reader :id
21
- attr_accessor :token, :unit_system
22
16
 
23
- def initialize(token:, id:, unit_system: IMPERIAL)
24
- raise InvalidParameterValue.new, "Invalid Units provided : #{unit_system}" unless UNITS.include?(unit_system)
25
- raise InvalidParameterValue.new, "Vehicle ID (id) is a required field" if id.nil?
26
- 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
27
74
  @token = token
28
75
  @id = id
29
- @unit_system = unit_system
30
- end
31
-
32
- # Class method Used to get all the vehicles in the app. This only returns
33
- # API - https://smartcar.com/docs/api#get-all-vehicles
34
- # @param token [String] - Access token
35
- # @param options [Hash] - Optional filter parameters (check documentation)
36
- #
37
- # @return [Array] of vehicle IDs(Strings)
38
- def self.all_vehicle_ids(token:, options: {})
39
- response, meta = new(token: token, id: 'none').fetch(
40
- path: PATH.call(''),
41
- options: options
42
- )
43
- response['vehicles']
44
- end
45
-
46
- # Class method Used to check compatiblity for VIN and scope
47
- # API - https://smartcar.com/docs/api#connect-compatibility
48
- # @param vin [String] VIN of the vehicle to be checked
49
- # @param scope [Array of Strings] - array of scopes
50
- # @param country [String] An optional country code according to
51
- # [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2).
52
- # Defaults to US.
53
- #
54
- # @return [Boolean] true or false
55
- def self.compatible?(vin:, scope:, country: 'US')
56
- raise InvalidParameterValue.new, "vin is a required field" if vin.nil?
57
- raise InvalidParameterValue.new, "scope is a required field" if scope.nil?
58
-
59
- response, meta = new(token: 'none', id: 'none').fetch(path: COMPATIBLITY_PATH,
60
- options: {
61
- vin: vin,
62
- scope: scope.join(' '),
63
- country: country
64
- },
65
- auth: BASIC
66
- )
67
- response['compatible']
68
- end
76
+ @unit_system = options[:unit_system]
77
+ @version = options[:version]
69
78
 
70
- # Method to get batch requests
71
- # API - https://smartcar.com/docs/api#post-batch-request
72
- # @param attributes [Array] Array of strings or symbols of attributes to be fetched together
73
- #
74
- # @return [Hash] Hash wth key as requested attribute(symbol) and value as Error OR Object of the requested attribute
75
- def batch(attributes = [])
76
- raise InvalidParameterValue.new, "vin is a required field" if attributes.nil?
77
- request_body = get_batch_request_body(attributes)
78
- response, _meta = post(PATH.call(id) + "/batch", request_body)
79
- process_batch_response(response)
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?
80
82
  end
81
83
 
82
- # Fetch the list of permissions that this application has been granted for
83
- # this vehicle
84
- # EX : Smartcar::Vehicle.new(token: token, id: id).permissions
85
- # @param options [Hash] - Optional filter parameters (check documentation)
86
- #
87
- # @return [Permissions] object
88
- def permissions(options: {})
89
- get_attribute(Permissions)
90
- end
91
-
92
- # Method Used toRevoke access for the current requesting application
93
- # API - https://smartcar.com/docs/api#delete-disconnect
94
- #
95
- # @return [Boolean] true if success
96
- def disconnect!
97
- response = delete(PATH.call(id) + "/application")
98
- response['status'] == SUCCESS
99
- end
100
-
101
- # Methods Used to lock car
102
- # API - https://smartcar.com/docs/api#post-security
103
- #
104
- # @return [Boolean] true if success
105
- def lock!
106
- lock_or_unlock!(action: Smartcar::LOCK)
107
- end
108
-
109
- # Methods Used to unlock car
110
- # API - https://smartcar.com/docs/api#post-security
111
- #
112
- # @return [Boolean] true if success
113
- def unlock!
114
- lock_or_unlock!(action: Smartcar::UNLOCK)
115
- end
116
-
117
- # Method used to start charging a car
84
+ # @!method attributes()
85
+ # Returns make model year and id of the vehicle
118
86
  #
87
+ # API Documentation - https://smartcar.com/api#get-vehicle-attributes
119
88
  #
120
- # @return [Boolean] true if success
121
- def start_charge!
122
- start_or_stop_charge!(action: Smartcar::START_CHARGE)
123
- 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.
124
91
 
125
- # Method used to stop 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.
126
94
  #
95
+ # API Documentation https://smartcar.com/docs/api#get-ev-battery
127
96
  #
128
- # @return [Boolean] true if success
129
- def stop_charge!
130
- start_or_stop_charge!(action: Smartcar::STOP_CHARGE)
131
- 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.
132
99
 
133
- # Returns make model year and id of the vehicle
134
- # API - https://smartcar.com/api#get-vehicle-attributes
100
+ # @!method battery_capacity()
101
+ # Returns the capacity of an electric or plug-in hybrid vehicle's battery.
135
102
  #
136
- # @return [VehicleAttributes] object
137
- def vehicle_attributes
138
- get_attribute(VehicleAttributes)
139
- end
140
-
141
- # Returns the state of charge (SOC) and remaining range of an electric or
142
- # plug-in hybrid vehicle's battery.
143
- # API - https://smartcar.com/docs/api#get-ev-battery
144
- #
145
- # @return [Battery] object
146
- def battery
147
- get_attribute(Battery)
148
- end
149
-
150
- # Returns the capacity of an electric or
151
- # plug-in hybrid vehicle's battery.
152
- # API - https://smartcar.com/docs/api#get-ev-battery-capacity
103
+ # API Documentation https://smartcar.com/docs/api#get-ev-battery-capacity
153
104
  #
154
- # @return [Battery] object
155
- def battery_capacity
156
- get_attribute(BatteryCapacity)
157
- 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.
158
107
 
108
+ # @!method charge()
159
109
  # Returns the current charge status of the vehicle.
160
- # API - https://smartcar.com/docs/api#get-ev-battery
161
110
  #
162
- # @return [Charge] object
163
- def charge
164
- get_attribute(Charge)
165
- 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.
166
115
 
116
+ # @!method engine_oil()
167
117
  # Returns the remaining life span of a vehicle's engine oil
168
- # API - https://smartcar.com/docs/api#get-engine-oil-life
169
118
  #
170
- # @return [EngineOil] object
171
- def engine_oil
172
- get_attribute(EngineOil)
173
- 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.
174
123
 
124
+ # @!method fuel()
175
125
  # Returns the status of the fuel remaining in the vehicle's gas tank.
176
- # API - https://smartcar.com/docs/api#get-fuel-tank
177
126
  #
178
- # @return [Fuel] object
179
- def fuel
180
- get_attribute(Fuel)
181
- 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.
182
131
 
132
+ # @!method location()
183
133
  # Returns the last known location of the vehicle in geographic coordinates.
184
- # API - https://smartcar.com/docs/api#get-location
185
134
  #
186
- # @return [Location] object
187
- def location
188
- get_attribute(Location)
189
- 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.
190
139
 
140
+ # @!method odometer()
191
141
  # Returns the vehicle's last known odometer reading.
192
- # API - https://smartcar.com/docs/api#get-odometer
193
142
  #
194
- # @return [Odometer] object
195
- def odometer
196
- get_attribute(Odometer)
197
- 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.
198
147
 
148
+ # @!method tire_pressure()
199
149
  # Returns the air pressure of each of the vehicle's tires.
200
- # API - https://smartcar.com/docs/api#get-tire-pressure
201
150
  #
202
- # @return [TirePressure] object
203
- def tire_pressure
204
- get_attribute(TirePressure)
205
- 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.
206
155
 
156
+ # @!method vin()
207
157
  # Returns the vehicle's manufacturer identifier (VIN).
208
- # API - https://smartcar.com/docs/api#get-vin
209
158
  #
210
- # @return [String] Vin of the vehicle.
211
- def vin
212
- _object = get_attribute(Vin)
213
- @vin ||= _object.vin
214
- end
215
-
216
- private
217
-
218
- def allowed_attributes
219
- @allowed_attributes ||= {
220
- battery: get_path(Battery),
221
- charge: get_path(Charge),
222
- engine_oil: get_path(EngineOil),
223
- fuel: get_path(Fuel),
224
- location: get_path(Location),
225
- odometer: get_path(Odometer),
226
- permissions: get_path(Permissions),
227
- tire_pressure: get_path(TirePressure),
228
- vin: get_path(Vin),
229
- }
230
- end
231
-
232
- def path_to_class
233
- @path_to_class ||= {
234
- get_path(Battery) => Battery,
235
- get_path(Charge) => Charge,
236
- get_path(EngineOil) => EngineOil,
237
- get_path(Fuel) => Fuel,
238
- get_path(Location) => Location,
239
- get_path(Odometer) => Odometer,
240
- get_path(Permissions) => Permissions,
241
- get_path(TirePressure) => TirePressure,
242
- get_path(Vin) => Vin,
243
- }
244
- end
245
-
246
- # @private
247
- BatchItemResponse = Struct.new(:body, :status, :headers) do
248
- def body_with_meta
249
- body.merge(meta: headers)
250
- end
251
- end
252
-
253
- def get_batch_request_body(attributes)
254
- attributes = validated_attributes(attributes)
255
- requests = attributes.each_with_object([]) do |item, requests|
256
- requests << { path: allowed_attributes[item] }
257
- end
258
- { requests: requests }
259
- end
260
-
261
- def process_batch_response(responses)
262
- inverted_map = allowed_attributes.invert
263
- responses["responses"].each_with_object({}) do |response, result|
264
- item_response = BatchItemResponse.new(response["body"], response["code"], response["headers"])
265
- error = get_error(item_response)
266
- path = response["path"]
267
- result[inverted_map[path]] = error || get_object(path_to_class[path], item_response.body_with_meta)
268
- end
269
- end
270
-
271
- def validated_attributes(attributes)
272
- attributes.map!(&:to_sym)
273
- unsupported_attributes = (attributes - allowed_attributes.keys) || []
274
- unless unsupported_attributes.empty?
275
- message = "Unsupported attribute(s) requested in batch - #{unsupported_attributes.join(',')}"
276
- 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])
277
185
  end
278
- attributes
279
186
  end
280
187
 
281
- def get_attribute(klass)
282
- body, meta = fetch(
283
- path: klass::PATH.call(id)
284
- )
285
- get_object(klass, body.merge(meta: meta))
286
- end
287
-
288
- def get_object(klass, data)
289
- 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)
290
198
  end
291
199
 
292
- def get_path(klass)
293
- path = klass::PATH.call(id)
294
- 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))
295
209
  end
296
210
 
297
- def lock_or_unlock!(action:)
298
- response, meta = post(PATH.call(id) + "/security", { action: action })
299
- 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)
300
224
  end
301
225
 
302
- def start_or_stop_charge!(action:)
303
- response, meta = post(PATH.call(id) + "/charge", { action: action })
304
- 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)
305
236
  end
306
237
  end
307
238
  end