From 42519ed83c07788f4e673b11bfaf61ff144f0a95 Mon Sep 17 00:00:00 2001 From: aytekin Date: Tue, 7 May 2024 17:09:35 -0500 Subject: [PATCH 1/2] feat: added support for service history API endpoint --- src/main/java/com/smartcar/sdk/ApiClient.java | 102 +++++++---- src/main/java/com/smartcar/sdk/Vehicle.java | 166 ++++++++++++------ .../com/smartcar/sdk/data/ServiceCost.java | 27 +++ .../com/smartcar/sdk/data/ServiceDetails.java | 17 ++ .../com/smartcar/sdk/data/ServiceHistory.java | 28 +++ .../com/smartcar/sdk/data/ServiceRecord.java | 71 ++++++++ .../com/smartcar/sdk/data/ServiceTask.java | 27 +++ .../java/com/smartcar/sdk/VehicleTest.java | 128 ++++++++------ src/test/resources/ServiceHistory.json | 64 +++++++ 9 files changed, 486 insertions(+), 144 deletions(-) create mode 100644 src/main/java/com/smartcar/sdk/data/ServiceCost.java create mode 100644 src/main/java/com/smartcar/sdk/data/ServiceDetails.java create mode 100644 src/main/java/com/smartcar/sdk/data/ServiceHistory.java create mode 100644 src/main/java/com/smartcar/sdk/data/ServiceRecord.java create mode 100644 src/main/java/com/smartcar/sdk/data/ServiceTask.java create mode 100644 src/test/resources/ServiceHistory.json diff --git a/src/main/java/com/smartcar/sdk/ApiClient.java b/src/main/java/com/smartcar/sdk/ApiClient.java index 3d7eaf72..e9a644be 100644 --- a/src/main/java/com/smartcar/sdk/ApiClient.java +++ b/src/main/java/com/smartcar/sdk/ApiClient.java @@ -1,12 +1,21 @@ package com.smartcar.sdk; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; import com.smartcar.sdk.data.ApiData; import com.smartcar.sdk.data.Meta; import okhttp3.*; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -15,7 +24,8 @@ abstract class ApiClient { public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); /** - * Retrieves the SDK version, falling back to DEVELOPMENT if we're not running from a jar. + * Retrieves the SDK version, falling back to DEVELOPMENT if we're not running + * from a jar. * * @return the SDK version */ @@ -29,35 +39,33 @@ private static String getSdkVersion() { return version; } - protected static final String USER_AGENT = - String.format( - "Smartcar/%s (%s; %s) Java v%s %s", - getSdkVersion(), - System.getProperty("os.name"), - System.getProperty("os.arch"), - System.getProperty("java.version"), - System.getProperty("java.vm.name")); + protected static final String USER_AGENT = String.format( + "Smartcar/%s (%s; %s) Java v%s %s", + getSdkVersion(), + System.getProperty("os.name"), + System.getProperty("os.arch"), + System.getProperty("java.version"), + System.getProperty("java.vm.name")); - private static final OkHttpClient client = - new OkHttpClient.Builder().readTimeout(310, TimeUnit.SECONDS).build(); + private static final OkHttpClient client = new OkHttpClient.Builder().readTimeout(310, TimeUnit.SECONDS).build(); - - static GsonBuilder gson = - new GsonBuilder().setFieldNamingStrategy((field) -> Utils.toCamelCase(field.getName())); + static GsonBuilder gson = new GsonBuilder().setFieldNamingStrategy((field) -> Utils.toCamelCase(field.getName())); /** - * Builds a request object with common headers, using provided request parameters - * @param url url for the request, including the query parameters - * @param method http method - * @param body request body + * Builds a request object with common headers, using provided request + * parameters + * + * @param url url for the request, including the query parameters + * @param method http method + * @param body request body * @param headers additional headers to set for the request * @return */ protected static Request buildRequest(HttpUrl url, String method, RequestBody body, Map headers) { Request.Builder request = new Request.Builder() - .url(url) - .addHeader("User-Agent", ApiClient.USER_AGENT) - .method(method, body); + .url(url) + .addHeader("User-Agent", ApiClient.USER_AGENT) + .method(method, body); headers.forEach(request::addHeader); @@ -86,11 +94,12 @@ protected static Response execute(Request request) throws SmartcarException { } /** - * Sends the specified request, parsing the response into the specified type. Wraps the request + * Sends the specified request, parsing the response into the specified type. + * Wraps the request * with the unitSystem and age meta data. * - * @param the data container for the parsed response JSON - * @param request the desired request to transmit + * @param the data container for the parsed response JSON + * @param request the desired request to transmit * @param dataType the type into which the response will be parsed * @return the wrapped response * @throws SmartcarException if the request is unsuccessful @@ -98,33 +107,58 @@ protected static Response execute(Request request) throws SmartcarException { protected static T execute( Request request, Class dataType) throws SmartcarException { Response response = ApiClient.execute(request); - T data; + T data = null; Meta meta; String bodyString = ""; try { bodyString = response.body().string(); - data = ApiClient.gson.create().fromJson(bodyString, dataType); + + JsonElement jsonElement = JsonParser.parseString(bodyString); + Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + + if (jsonElement.isJsonArray()) { + Field itemsField = dataType.getDeclaredField("items"); + itemsField.setAccessible(true); + + Type genericFieldType = itemsField.getGenericType(); + if (genericFieldType instanceof ParameterizedType) { + ParameterizedType pType = (ParameterizedType) genericFieldType; + Type[] fieldArgTypes = pType.getActualTypeArguments(); + if (fieldArgTypes.length > 0) { + Type listTypeArgument = fieldArgTypes[0]; + + Type listType = TypeToken.getParameterized(List.class, listTypeArgument).getType(); + List list = gson.fromJson(jsonElement, listType); + + T dataInstance = dataType.getDeclaredConstructor().newInstance(); + itemsField.set(dataInstance, list); + data = dataInstance; // Depending on your method's return type and needs + } + } + } else { + data = ApiClient.gson.create().fromJson(bodyString, dataType); + } + Headers headers = response.headers(); JsonObject headerJson = new JsonObject(); - for (String header: response.headers().names()) { + for (String header : headers.names()) { headerJson.addProperty(header.toLowerCase(), headers.get(header)); } String headerJsonString = headerJson.toString(); meta = ApiClient.gson.create().fromJson(headerJsonString, Meta.class); data.setMeta(meta); + return data; } catch (Exception ex) { if (bodyString.equals("")) { bodyString = "Empty response body"; } throw new SmartcarException.Builder() - .statusCode(response.code()) - .description(bodyString) - .requestId(response.headers().get("sc-request-id")) - .type("SDK_ERROR") - .build(); + .statusCode(response.code()) + .description(bodyString) + .requestId(response.headers().get("sc-request-id")) + .type("SDK_ERROR") + .build(); } - - return data; } } diff --git a/src/main/java/com/smartcar/sdk/Vehicle.java b/src/main/java/com/smartcar/sdk/Vehicle.java index 112fbc2c..3ba35918 100644 --- a/src/main/java/com/smartcar/sdk/Vehicle.java +++ b/src/main/java/com/smartcar/sdk/Vehicle.java @@ -10,10 +10,15 @@ import javax.json.JsonArrayBuilder; import javax.json.JsonObject; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; - /** Smartcar Vehicle API Object */ public class Vehicle { public enum UnitSystem { @@ -37,7 +42,7 @@ public enum UnitSystem { /** * Initializes a new Vehicle. * - * @param vehicleId the vehicle ID + * @param vehicleId the vehicle ID * @param accessToken the OAuth 2.0 access token */ public Vehicle(String vehicleId, String accessToken) { @@ -46,9 +51,11 @@ public Vehicle(String vehicleId, String accessToken) { /** * Initializes a new Vehicle with provided options - * @param vehicleId vehicleId the vehicle ID + * + * @param vehicleId vehicleId the vehicle ID * @param accessToken accessToken the OAuth 2.0 access token - * @param options optional arguments provided with a SmartcarVehicleOptions instance + * @param options optional arguments provided with a SmartcarVehicleOptions + * instance */ public Vehicle(String vehicleId, String accessToken, SmartcarVehicleOptions options) { this.vehicleId = vehicleId; @@ -61,40 +68,40 @@ public Vehicle(String vehicleId, String accessToken, SmartcarVehicleOptions opti /** * Gets the version of Smartcar API that this vehicle is using + * * @return String representing version */ public String getVersion() { return this.version; } - /** + /** * Gets the flags that are passed to the vehicle object as a serialized string + * * @return serialized string of the flags */ - public String getFlags(){ + public String getFlags() { return this.flags; } /** * Executes an API request under the VehicleIds endpoint. * - * @param path the path to the sub-endpoint + * @param path the path to the sub-endpoint * @param method the method of the request - * @param body the body of the request - * @param type the type into which the response will be parsed + * @param body the body of the request + * @param type the type into which the response will be parsed * @return the parsed response * @throws SmartcarException if the request is unsuccessful */ protected T call( String path, String method, RequestBody body, String accessToken, Class type) throws SmartcarException { - HttpUrl.Builder urlBuilder = - HttpUrl.parse(this.origin) - .newBuilder() - .addPathSegments("v" + this.version) - .addPathSegments("vehicles") - .addPathSegments(this.vehicleId) - .addPathSegments(path); - + HttpUrl.Builder urlBuilder = HttpUrl.parse(this.origin) + .newBuilder() + .addPathSegments("v" + this.version) + .addPathSegments("vehicles") + .addPathSegments(this.vehicleId) + .addPathSegments(path); if (this.getFlags() != null) { urlBuilder.addQueryParameter("flags", this.getFlags()); @@ -109,21 +116,22 @@ protected T call( return ApiClient.execute(request, type); } - protected T call(String path, String method, RequestBody body, Class type) throws SmartcarException{ + protected T call(String path, String method, RequestBody body, Class type) + throws SmartcarException { return this.call(path, method, body, this.accessToken, type); } - protected T call(String path, String method, RequestBody body, Map query, Class type) - throws SmartcarException { - HttpUrl.Builder urlBuilder = - HttpUrl.parse(this.origin) - .newBuilder() - .addPathSegments("v" + this.version) - .addPathSegments("vehicles") - .addPathSegments(this.vehicleId) - .addPathSegments(path); + protected T call(String path, String method, RequestBody body, Map query, + Class type) + throws SmartcarException { + HttpUrl.Builder urlBuilder = HttpUrl.parse(this.origin) + .newBuilder() + .addPathSegments("v" + this.version) + .addPathSegments("vehicles") + .addPathSegments(this.vehicleId) + .addPathSegments(path); - for (Map.Entry entry: query.entrySet()) { + for (Map.Entry entry : query.entrySet()) { urlBuilder.addQueryParameter(entry.getKey(), entry.getValue()); } if (this.getFlags() != null) { @@ -170,8 +178,7 @@ public ApplicationPermissions permissions() throws SmartcarException { return this.permissions; } - this.permissions = - this.call("permissions", "GET", null, ApplicationPermissions.class); + this.permissions = this.call("permissions", "GET", null, ApplicationPermissions.class); return this.permissions; } @@ -211,6 +218,52 @@ public VehicleOdometer odometer() throws SmartcarException { return this.call("odometer", "GET", null, VehicleOdometer.class); } + /** + * Returns a list of all the service records performed on the vehicle, + * filtered by the optional date range. If no dates are specified, records from + * the last year are returned. + * + * @param startDate the start date of the period to retrieve records from + * (inclusive) + * @param endDate the end date of the period to retrieve records until + * (inclusive) + * @return service history records + * @throws SmartcarException if the request is unsuccessful + * @throws UnsupportedEncodingException + */ + public ServiceHistory serviceHistory(OffsetDateTime startDate, OffsetDateTime endDate) + throws SmartcarException, UnsupportedEncodingException { + if (startDate == null || endDate == null) { + // If dates are not specified, default to the last year + endDate = OffsetDateTime.now(ZoneOffset.UTC); + startDate = endDate.minusYears(1); + } + + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + String formattedStartDate = startDate.format(dateFormatter); + String formattedEndDate = endDate.format(dateFormatter); + + String encodedStartDate = URLEncoder.encode(formattedStartDate, StandardCharsets.UTF_8.toString()); + String encodedEndDate = URLEncoder.encode(formattedEndDate, StandardCharsets.UTF_8.toString()); + + String url = "service/history?start_date=" + encodedStartDate + "&end_date=" + encodedEndDate; + + return this.call(url, "GET", null, ServiceHistory.class); + } + + /** + * Overload without parameters to handle no input case, calling the full method + * with nulls + * + * @return service history records + * @throws SmartcarException if the request is unsuccessful + * @throws UnsupportedEncodingException + */ + public ServiceHistory serviceHistory() throws SmartcarException, UnsupportedEncodingException { + return serviceHistory(null, null); + } + /** * Send request to the /fuel endpoint * @@ -373,14 +426,17 @@ public VehicleLockStatus lockStatus() throws SmartcarException { } /** - * Send request to the /navigation/destination endpoint to set the navigation destination + * Send request to the /navigation/destination endpoint to set the navigation + * destination * - * @param latitude A double representing the destination's latitude + * @param latitude A double representing the destination's latitude * @param longitude A double representing the destination's longitude * @return a response indicating success - * @throws SmartcarException if the request is unsuccessful - * @throws IllegalArgumentException if the latitude is not between -90.0 and 90.0 or - * if the longitude is not between -180.0 and 180.0 + * @throws SmartcarException if the request is unsuccessful + * @throws IllegalArgumentException if the latitude is not between -90.0 and + * 90.0 or + * if the longitude is not between -180.0 and + * 180.0 */ public ActionResponse sendDestination(double latitude, double longitude) throws SmartcarException { if (latitude < LATITUDE_MIN || latitude > LATITUDE_MAX) { @@ -402,8 +458,6 @@ public ActionResponse sendDestination(double latitude, double longitude) throws return this.call("navigation/destination", "POST", requestBody, ActionResponse.class); } - - /** * Subscribe vehicle to a webhook * @@ -411,7 +465,7 @@ public ActionResponse sendDestination(double latitude, double longitude) throws * @throws SmartcarException if the request is unsuccessful */ public WebhookSubscription subscribe(String webhookId) throws SmartcarException { - RequestBody body = RequestBody.create(null, new byte[]{}); + RequestBody body = RequestBody.create(null, new byte[] {}); return this.call("webhooks/" + webhookId, "POST", body, WebhookSubscription.class); } @@ -428,8 +482,10 @@ public UnsubscribeResponse unsubscribe(String applicationManagementToken, String /** * Send request to the /batch endpoint * - * @param paths the paths of endpoints to send requests to (ex. "/odometer", "/location", ...) - * @return the BatchResponse object containing the response from all the requested endpoints + * @param paths the paths of endpoints to send requests to (ex. "/odometer", + * "/location", ...) + * @return the BatchResponse object containing the response from all the + * requested endpoints * @throws SmartcarException if the request is unsuccessful */ public BatchResponse batch(String[] paths) throws SmartcarException { @@ -444,8 +500,7 @@ public BatchResponse batch(String[] paths) throws SmartcarException { ApiClient.gson.registerTypeAdapter(BatchResponse.class, new BatchDeserializer()); RequestBody body = RequestBody.create(ApiClient.JSON, json.toString()); - BatchResponse response = - this.call("batch", "POST", body, BatchResponse.class); + BatchResponse response = this.call("batch", "POST", body, BatchResponse.class); BatchResponse batchResponse = response; batchResponse.setRequestId(response.getMeta().getRequestId()); return batchResponse; @@ -453,20 +508,21 @@ public BatchResponse batch(String[] paths) throws SmartcarException { /** * General purpose method to make a request to a Smartcar endpoint - can be used - * to make requests to brand specific endpoints. + * to make requests to brand specific endpoints. * - * @param vehicleRequest with options for this request. See Smartcar.SmartcarVehicleRequest - * @return the VehicleResponse object containing the response from the requested endpoint + * @param vehicleRequest with options for this request. See + * Smartcar.SmartcarVehicleRequest + * @return the VehicleResponse object containing the response from the requested + * endpoint * @throws SmartcarException if the request is unsuccessful */ public VehicleResponse request(SmartcarVehicleRequest vehicleRequest) throws SmartcarException, IOException { - HttpUrl.Builder urlBuilder = - HttpUrl.parse(this.origin) - .newBuilder() - .addPathSegments("v" + this.version) - .addPathSegments("vehicles") - .addPathSegments(this.vehicleId) - .addPathSegments(vehicleRequest.getPath()); + HttpUrl.Builder urlBuilder = HttpUrl.parse(this.origin) + .newBuilder() + .addPathSegments("v" + this.version) + .addPathSegments("vehicles") + .addPathSegments(this.vehicleId) + .addPathSegments(vehicleRequest.getPath()); if (this.flags != null) { urlBuilder.addQueryParameter("flags", this.flags); @@ -489,9 +545,9 @@ public VehicleResponse request(SmartcarVehicleRequest vehicleRequest) throws Sma headers.putAll(vehicleRequest.getHeaders()); Request request = ApiClient.buildRequest(url, - vehicleRequest.getMethod(), - vehicleRequest.getBody(), - headers); + vehicleRequest.getMethod(), + vehicleRequest.getBody(), + headers); ApiClient.gson.registerTypeAdapter(VehicleResponse.class, new VehicleResponseDeserializer()); diff --git a/src/main/java/com/smartcar/sdk/data/ServiceCost.java b/src/main/java/com/smartcar/sdk/data/ServiceCost.java new file mode 100644 index 00000000..72c66e00 --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/ServiceCost.java @@ -0,0 +1,27 @@ +package com.smartcar.sdk.data; + +public class ServiceCost { + private double totalCost; + private String currency; + + public ServiceCost(double totalCost, String currency) { + this.totalCost = totalCost; + this.currency = currency; + } + + public double getTotalCost() { + return totalCost; + } + + public void setTotalCost(double totalCost) { + this.totalCost = totalCost; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } +} diff --git a/src/main/java/com/smartcar/sdk/data/ServiceDetails.java b/src/main/java/com/smartcar/sdk/data/ServiceDetails.java new file mode 100644 index 00000000..f378cba7 --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/ServiceDetails.java @@ -0,0 +1,17 @@ +package com.smartcar.sdk.data; + +public class ServiceDetails { + private String type; + + public ServiceDetails(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/src/main/java/com/smartcar/sdk/data/ServiceHistory.java b/src/main/java/com/smartcar/sdk/data/ServiceHistory.java new file mode 100644 index 00000000..6a4fe550 --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/ServiceHistory.java @@ -0,0 +1,28 @@ +package com.smartcar.sdk.data; + +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +/** POJO for Service History of a Vehicle */ +public class ServiceHistory extends ApiData { + @SerializedName("items") + private List items; + + public ServiceHistory() { + // no-arg constructor for deserialization + } + + public ServiceHistory(List items) { + this.items = items; + } + + /** + * Returns the list of service records. + * + * @return List of service records + */ + public List getItems() { + return this.items; + } +} diff --git a/src/main/java/com/smartcar/sdk/data/ServiceRecord.java b/src/main/java/com/smartcar/sdk/data/ServiceRecord.java new file mode 100644 index 00000000..ccee449a --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/ServiceRecord.java @@ -0,0 +1,71 @@ +package com.smartcar.sdk.data; + +import java.util.List; + +public class ServiceRecord { + private int serviceId; + private String serviceDate; + private int odometerDistance; + private List serviceTasks; + private ServiceDetails serviceDetails; + private ServiceCost serviceCost; + + public ServiceRecord(int serviceId, String serviceDate, int odometerDistance, + List serviceTasks, ServiceDetails serviceDetails, + ServiceCost serviceCost) { + this.serviceId = serviceId; + this.serviceDate = serviceDate; + this.odometerDistance = odometerDistance; + this.serviceTasks = serviceTasks; + this.serviceDetails = serviceDetails; + this.serviceCost = serviceCost; + } + + public int getServiceId() { + return serviceId; + } + + public void setServiceId(int serviceId) { + this.serviceId = serviceId; + } + + public String getServiceDate() { + return serviceDate; + } + + public void setServiceDate(String serviceDate) { + this.serviceDate = serviceDate; + } + + public int getOdometerDistance() { + return odometerDistance; + } + + public void setOdometerDistance(int odometerDistance) { + this.odometerDistance = odometerDistance; + } + + public List getServiceTasks() { + return serviceTasks; + } + + public void setServiceTasks(List serviceTasks) { + this.serviceTasks = serviceTasks; + } + + public ServiceDetails getServiceDetails() { + return serviceDetails; + } + + public void setServiceDetails(ServiceDetails serviceDetails) { + this.serviceDetails = serviceDetails; + } + + public ServiceCost getServiceCost() { + return serviceCost; + } + + public void setServiceCost(ServiceCost serviceCost) { + this.serviceCost = serviceCost; + } +} diff --git a/src/main/java/com/smartcar/sdk/data/ServiceTask.java b/src/main/java/com/smartcar/sdk/data/ServiceTask.java new file mode 100644 index 00000000..95bcc2fd --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/ServiceTask.java @@ -0,0 +1,27 @@ +package com.smartcar.sdk.data; + +class ServiceTask { + private int taskId; + private String taskDescription; + + public ServiceTask(int taskId, String taskDescription) { + this.taskId = taskId; + this.taskDescription = taskDescription; + } + + public int getTaskId() { + return taskId; + } + + public void setTaskId(int taskId) { + this.taskId = taskId; + } + + public String getTaskDescription() { + return taskDescription; + } + + public void setTaskDescription(String taskDescription) { + this.taskDescription = taskDescription; + } +} \ No newline at end of file diff --git a/src/test/java/com/smartcar/sdk/VehicleTest.java b/src/test/java/com/smartcar/sdk/VehicleTest.java index ce8125b1..3cf36c43 100644 --- a/src/test/java/com/smartcar/sdk/VehicleTest.java +++ b/src/test/java/com/smartcar/sdk/VehicleTest.java @@ -14,6 +14,8 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.Date; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -43,31 +45,31 @@ private JsonElement loadJsonResource(String resourceName) throws FileNotFoundExc private void loadAndEnqueueErrorResponse(String resource, int statusCode) throws FileNotFoundException { JsonElement error = loadJsonResource(resource); MockResponse mockResponse = new MockResponse() - .setResponseCode(statusCode) - .setBody(error.toString()) - .addHeader("sc-request-id", this.expectedRequestId) - .addHeader("content-type", "application/json"); + .setResponseCode(statusCode) + .setBody(error.toString()) + .addHeader("sc-request-id", this.expectedRequestId) + .addHeader("content-type", "application/json"); TestExecutionListener.mockWebServer.enqueue(mockResponse); } private void loadAndEnqueueRateLimitErrorResponse(int retryAfter) throws FileNotFoundException { JsonElement error = loadJsonResource("ErrorVehicleRateLimit"); MockResponse mockResponse = new MockResponse() - .setResponseCode(429) - .setBody(error.toString()) - .addHeader("sc-request-id", this.expectedRequestId) - .addHeader("content-type", "application/json") - .addHeader("retry-after", retryAfter); + .setResponseCode(429) + .setBody(error.toString()) + .addHeader("sc-request-id", this.expectedRequestId) + .addHeader("content-type", "application/json") + .addHeader("retry-after", retryAfter); TestExecutionListener.mockWebServer.enqueue(mockResponse); } private void loadAndEnqueueResponse(String resourceName) throws FileNotFoundException { JsonElement success = loadJsonResource(resourceName); MockResponse mockResponse = new MockResponse() - .setBody(success.toString()) - .addHeader("sc-request-id", this.expectedRequestId) - .addHeader("sc-data-age", this.dataAge) - .addHeader("sc-unit-system", this.unitSystem); + .setBody(success.toString()) + .addHeader("sc-request-id", this.expectedRequestId) + .addHeader("sc-data-age", this.dataAge) + .addHeader("sc-unit-system", this.unitSystem); TestExecutionListener.mockWebServer.enqueue(mockResponse); } @@ -75,10 +77,10 @@ private void loadAndEnqueueResponse(String resourceName) throws FileNotFoundExce private void beforeMethod() throws IOException { SmartcarVehicleOptions options = new SmartcarVehicleOptions.Builder() - .addFlag("foo", "bar") - .addFlag("test", true) - .origin("http://localhost:" + TestExecutionListener.mockWebServer.getPort()) - .build(); + .addFlag("foo", "bar") + .addFlag("test", true) + .origin("http://localhost:" + TestExecutionListener.mockWebServer.getPort()) + .build(); this.subject = new Vehicle(this.vehicleId, this.accessToken, options); } @@ -96,10 +98,10 @@ public void testMeta() throws Exception { @Test public void testMetaNull() throws SmartcarException { MockResponse mockResponse = new MockResponse() - .setResponseCode(200) - .setBody("{ 'distance': 100 }") - .addHeader("sc-request-id", this.expectedRequestId) - .addHeader("sc-unit-system", this.unitSystem); + .setResponseCode(200) + .setBody("{ 'distance': 100 }") + .addHeader("sc-request-id", this.expectedRequestId) + .addHeader("sc-unit-system", this.unitSystem); TestExecutionListener.mockWebServer.enqueue(mockResponse); VehicleOdometer odo = this.subject.odometer(); @@ -163,6 +165,18 @@ public void testOdometer() throws Exception { Assert.assertEquals(odometer.getDistance(), 104.32); } + @Test + public void testServiceHistory() throws Exception { + loadAndEnqueueResponse("ServiceHistory"); + + OffsetDateTime startDate = OffsetDateTime.of(2023, 5, 20, 0, 0, 0, 0, ZoneOffset.UTC); + OffsetDateTime endDate = OffsetDateTime.of(2024, 2, 10, 0, 0, 0, 0, ZoneOffset.UTC); + + ServiceHistory serviceHistory = this.subject.serviceHistory(startDate, endDate); + + Assert.assertEquals(serviceHistory.getItems().size(), 3); + } + @Test public void testFuel() throws Exception { loadAndEnqueueResponse("GetFuel"); @@ -374,7 +388,7 @@ public void testSendDestinationIllegalArgumentLatitude() { @Test public void testSendDestinationIllegalArgumentLongitude() { Assert.assertThrows(IllegalArgumentException.class, - () -> this.subject.sendDestination(47.6205063, -192.3518523)); + () -> this.subject.sendDestination(47.6205063, -192.3518523)); } @Test @@ -399,11 +413,11 @@ public void testRequestOdometer() throws Exception { loadAndEnqueueResponse("GetOdometer"); SmartcarVehicleRequest request = new SmartcarVehicleRequest.Builder() - .method("GET") - .path("odometer") - .addHeader("sc-unit-system", "imperial") - .addFlag("foo", "bar") - .build(); + .method("GET") + .path("odometer") + .addHeader("sc-unit-system", "imperial") + .addFlag("foo", "bar") + .build(); VehicleResponse odometer = this.subject.request(request); @@ -417,7 +431,7 @@ public void testRequestOdometer() throws Exception { public void testRequestBatch() throws Exception { loadAndEnqueueResponse("BatchResponseSuccess"); - String[] paths = new String[]{"/odometer", "/tires/pressure"}; + String[] paths = new String[] { "/odometer", "/tires/pressure" }; JsonArrayBuilder endpoints = Json.createArrayBuilder(); for (String path : paths) { endpoints.add(Json.createObjectBuilder().add("path", path)); @@ -425,11 +439,11 @@ public void testRequestBatch() throws Exception { javax.json.JsonArray requests = endpoints.build(); SmartcarVehicleRequest request = new SmartcarVehicleRequest.Builder() - .method("POST") - .path("batch") - .addBodyParameter("requests", requests) - .addHeader("sc-unit-system", "imperial") - .build(); + .method("POST") + .path("batch") + .addBodyParameter("requests", requests) + .addHeader("sc-unit-system", "imperial") + .build(); VehicleResponse batchResponse = this.subject.request(request); Assert.assertEquals(batchResponse.getMeta().getRequestId(), "67127d3a-a08a-41f0-8211-f96da36b2d6e"); @@ -450,7 +464,7 @@ public void testRequestBatch() throws Exception { public void testLockStatusBatch() throws Exception { loadAndEnqueueResponse("BatchLockStatusResponseSuccess"); - String[] paths = new String[]{"/security"}; + String[] paths = new String[] { "/security" }; JsonArrayBuilder endpoints = Json.createArrayBuilder(); for (String path : paths) { endpoints.add(Json.createObjectBuilder().add("path", path)); @@ -458,10 +472,10 @@ public void testLockStatusBatch() throws Exception { javax.json.JsonArray requests = endpoints.build(); SmartcarVehicleRequest request = new SmartcarVehicleRequest.Builder() - .method("POST") - .path("batch") - .addBodyParameter("requests", requests) - .build(); + .method("POST") + .path("batch") + .addBodyParameter("requests", requests) + .build(); VehicleResponse batchResponse = this.subject.request(request); Assert.assertEquals(batchResponse.getMeta().getRequestId(), "67127d3a-a08a-41f0-8211-f96da36b2d6e"); @@ -530,7 +544,8 @@ public void testV2PermissionError() throws FileNotFoundException { } catch (SmartcarException ex) { thrown = true; Assert.assertEquals(ex.getStatusCode(), 403); - Assert.assertEquals(ex.getDescription(), "Your application has insufficient permissions to access the requested resource. Please prompt the user to re-authenticate using Smartcar Connect."); + Assert.assertEquals(ex.getDescription(), + "Your application has insufficient permissions to access the requested resource. Please prompt the user to re-authenticate using Smartcar Connect."); Assert.assertEquals(ex.getType(), "PERMISSION"); Assert.assertEquals(ex.getDocURL(), "https://smartcar.com/docs/errors/v2.0/other-errors/#permission"); Assert.assertEquals(ex.getResolutionType(), "REAUTHENTICATE"); @@ -542,7 +557,7 @@ public void testV2PermissionError() throws FileNotFoundException { } @Test - public void testV2VehicleStateError() throws FileNotFoundException { + public void testV2VehicleStateError() throws FileNotFoundException { loadAndEnqueueErrorResponse("ErrorVehicleStateV2", 409); boolean thrown = false; @@ -551,7 +566,8 @@ public void testV2VehicleStateError() throws FileNotFoundException { } catch (SmartcarException ex) { thrown = true; Assert.assertEquals(ex.getStatusCode(), 409); - Assert.assertEquals(ex.getDescription(), "The vehicle is in a sleep state and temporarily unable to perform your request."); + Assert.assertEquals(ex.getDescription(), + "The vehicle is in a sleep state and temporarily unable to perform your request."); Assert.assertEquals(ex.getType(), "VEHICLE_STATE"); Assert.assertEquals(ex.getDocURL(), "https://smartcar.com/docs/errors/v2.0/vehicle-state/#asleep"); Assert.assertNull(ex.getResolutionType()); @@ -561,8 +577,9 @@ public void testV2VehicleStateError() throws FileNotFoundException { Assert.assertTrue(thrown); } + @Test - public void testV2RateLimitError() throws FileNotFoundException { + public void testV2RateLimitError() throws FileNotFoundException { loadAndEnqueueRateLimitErrorResponse(12345); boolean thrown = false; @@ -572,7 +589,8 @@ public void testV2RateLimitError() throws FileNotFoundException { thrown = true; Assert.assertEquals(ex.getStatusCode(), 429); Assert.assertEquals(ex.getRetryAfter(), 12345); - Assert.assertEquals(ex.getSuggestedUserMessage(), "Your vehicle is temporarily unable to connect to KabobMobile. Please be patient while we’re working to resolve this issue."); + Assert.assertEquals(ex.getSuggestedUserMessage(), + "Your vehicle is temporarily unable to connect to KabobMobile. Please be patient while we’re working to resolve this issue."); } Assert.assertTrue(thrown); @@ -581,11 +599,11 @@ public void testV2RateLimitError() throws FileNotFoundException { @Test public void testInvalidJsonResponse() { MockResponse mockResponse = new MockResponse() - .setResponseCode(500) - .setBody("{ \"InvalidJSON\": {") - .addHeader("sc-request-id", this.expectedRequestId) - .addHeader("sc-data-age", this.dataAge) - .addHeader("sc-unit-system", this.unitSystem); + .setResponseCode(500) + .setBody("{ \"InvalidJSON\": {") + .addHeader("sc-request-id", this.expectedRequestId) + .addHeader("sc-data-age", this.dataAge) + .addHeader("sc-unit-system", this.unitSystem); TestExecutionListener.mockWebServer.enqueue(mockResponse); boolean thrown = false; @@ -605,7 +623,7 @@ public void testInvalidJsonResponse() { @Test public void testNullErrorResponse() { MockResponse mockResponse = new MockResponse() - .setResponseCode(500); + .setResponseCode(500); TestExecutionListener.mockWebServer.enqueue(mockResponse); boolean thrown = false; @@ -624,7 +642,7 @@ public void testNullErrorResponse() { @Test public void testNull200Response() { MockResponse mockResponse = new MockResponse() - .setResponseCode(200); + .setResponseCode(200); TestExecutionListener.mockWebServer.enqueue(mockResponse); boolean thrown = false; @@ -693,7 +711,7 @@ public void testBatch() throws Exception { String expectedRequestId = "67127d3a-a08a-41f0-8211-f96da36b2d6e"; loadAndEnqueueResponse("BatchResponseSuccess"); - BatchResponse batch = this.subject.batch(new String[] {"/odometer"}); + BatchResponse batch = this.subject.batch(new String[] { "/odometer" }); Assert.assertEquals(batch.getRequestId(), expectedRequestId); boolean thrown = false; try { @@ -715,7 +733,7 @@ public void testBatch() throws Exception { public void testBatchHTTPError() throws Exception { loadAndEnqueueResponse("BatchResponseError"); - BatchResponse batch = this.subject.batch(new String[] {"/odometer"}); + BatchResponse batch = this.subject.batch(new String[] { "/odometer" }); boolean thrown = false; try { @@ -737,7 +755,7 @@ public void testBatchHTTPError() throws Exception { public void testBatchHTTPErrorV2() throws Exception { loadAndEnqueueResponse("BatchResponseErrorV2"); - BatchResponse batch = this.subject.batch(new String[] {"/odometer"}); + BatchResponse batch = this.subject.batch(new String[] { "/odometer" }); boolean thrown = false; try { @@ -762,7 +780,7 @@ public void testBatchHTTPErrorV2() throws Exception { public void testBatchHTTPErrorV2WithDetail() throws Exception { loadAndEnqueueResponse("BatchResponseErrorV2WithDetail"); - BatchResponse batch = this.subject.batch(new String[] {"/odometer"}); + BatchResponse batch = this.subject.batch(new String[] { "/odometer" }); boolean thrown = false; try { @@ -801,7 +819,7 @@ public void testBatchMixedErrorsSuccess() throws FileNotFoundException, Smartcar loadAndEnqueueResponse("BatchResponseMixed"); boolean thrown = false; - BatchResponse batch = this.subject.batch(new String[] {"/odometer", "/fuel"}); + BatchResponse batch = this.subject.batch(new String[] { "/odometer", "/fuel" }); VehicleOdometer odo = batch.odometer(); try { batch.fuel(); diff --git a/src/test/resources/ServiceHistory.json b/src/test/resources/ServiceHistory.json new file mode 100644 index 00000000..b5185e22 --- /dev/null +++ b/src/test/resources/ServiceHistory.json @@ -0,0 +1,64 @@ +[ + { + "serviceId": 12345, + "serviceDate": "2022-07-10T16:20:00.000Z", + "odometerDistance": 50000, + "serviceTasks": [ + { + "taskId": 1, + "taskDescription": "oil change" + } + ], + "serviceDetails": { + "type": "dealership" + }, + "serviceCost": { + "totalCost": 100, + "currency": "USD" + } + }, + { + "serviceId": 23456, + "serviceDate": "2023-01-15T10:30:00.000Z", + "odometerDistance": 62000, + "serviceTasks": [ + { + "taskId": 2, + "taskDescription": "tire rotation" + }, + { + "taskId": 3, + "taskDescription": "air filter replacement" + } + ], + "serviceDetails": { + "type": "independent garage" + }, + "serviceCost": { + "totalCost": 150, + "currency": "USD" + } + }, + { + "serviceId": 34567, + "serviceDate": "2023-04-20T14:45:00.000Z", + "odometerDistance": 73000, + "serviceTasks": [ + { + "taskId": 4, + "taskDescription": "brake pad replacement" + }, + { + "taskId": 5, + "taskDescription": "coolant flush" + } + ], + "serviceDetails": { + "type": "specialist" + }, + "serviceCost": { + "totalCost": 300, + "currency": "USD" + } + } +] \ No newline at end of file From dc0acfae89f9d99444c1bde3fd2b6f5677976d8d Mon Sep 17 00:00:00 2001 From: aytekin Date: Wed, 8 May 2024 13:51:09 -0500 Subject: [PATCH 2/2] feat: version bump --- README.md | 14 +++++++------- gradle.properties | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d9594e38..6f651044 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The recommended method for obtaining the SDK is via Gradle or Maven through the ### Gradle ```groovy -compile "com.smartcar.sdk:java-sdk:4.4.0" +compile "com.smartcar.sdk:java-sdk:4.5.0" ``` ### Maven @@ -18,16 +18,16 @@ compile "com.smartcar.sdk:java-sdk:4.4.0" com.smartcar.sdk java-sdk - 4.4.0 + 4.5.0 ``` ### Jar Direct Download -* [java-sdk-4.4.0.jar](https://repo1.maven.org/maven2/com/smartcar/sdk/java-sdk/4.4.0/java-sdk-4.4.0.jar) -* [java-sdk-4.4.0-sources.jar](https://repo1.maven.org/maven2/com/smartcar/sdk/java-sdk/4.4.0/java-sdk-4.4.0-sources.jar) -* [java-sdk-4.4.0-javadoc.jar](https://repo1.maven.org/maven2/com/smartcar/sdk/java-sdk/4.4.0/java-sdk-4.4.0-javadoc.jar) +* [java-sdk-4.5.0.jar](https://repo1.maven.org/maven2/com/smartcar/sdk/java-sdk/4.5.0/java-sdk-4.5.0.jar) +* [java-sdk-4.5.0-sources.jar](https://repo1.maven.org/maven2/com/smartcar/sdk/java-sdk/4.5.0/java-sdk-4.5.0-sources.jar) +* [java-sdk-4.5.0-javadoc.jar](https://repo1.maven.org/maven2/com/smartcar/sdk/java-sdk/4.5.0/java-sdk-4.5.0-javadoc.jar) -Signatures and other downloads available at [Maven Central](https://central.sonatype.com/artifact/com.smartcar.sdk/java-sdk/4.4.0). +Signatures and other downloads available at [Maven Central](https://central.sonatype.com/artifact/com.smartcar.sdk/java-sdk/4.5.0). ## Usage @@ -136,7 +136,7 @@ In accordance with the Semantic Versioning specification, the addition of suppor [ci-url]: https://travis-ci.com/smartcar/java-sdk [coverage-image]: https://codecov.io/gh/smartcar/java-sdk/branch/master/graph/badge.svg?token=nZAITx7w3X [coverage-url]: https://codecov.io/gh/smartcar/java-sdk -[javadoc-image]: https://img.shields.io/badge/javadoc-4.4.0-brightgreen.svg +[javadoc-image]: https://img.shields.io/badge/javadoc-4.5.0-brightgreen.svg [javadoc-url]: https://smartcar.github.io/java-sdk [maven-image]: https://img.shields.io/maven-central/v/com.smartcar.sdk/java-sdk.svg?label=Maven%20Central [maven-url]: https://central.sonatype.com/artifact/com.smartcar.sdk/java-sdk diff --git a/gradle.properties b/gradle.properties index 09d5df1e..2bbbdf0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ libGroup=com.smartcar.sdk libName=java-sdk -libVersion=4.4.0 +libVersion=4.5.0 libDescription=Smartcar Java SDK