Skip to content

Commit 4540691

Browse files
committed
task: refactor API to reduce duplication of code
1 parent 86fc763 commit 4540691

File tree

18 files changed

+164
-294
lines changed

18 files changed

+164
-294
lines changed

README.md

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ Config.environment = Environment.PRODUCTION // For production
4141
```
4242

4343
### API object
44-
Interaction with the APIs is done via the `Api` class. This class exposes a number of factory methods for instantiating an `Api` object with either a default Ktor client, or a customised one.
44+
Interaction with the APIs is done via the `Api` singleton. The Api singleton provides access to a configured Ktor client for interaction with the Api. A session token can also be applied:
4545

46-
```Api.create()```
46+
```kotlin
47+
Api.sessionToken = "valid_session_token"
48+
```
4749

4850
### Routers
4951
Routers are used to encapsulate specific areas of the API. For example, the `VehiclesRouter` takes care of all Vehicle API interactions. Routers are attached to the `Api` object via extension properties.
@@ -52,27 +54,21 @@ Routers are used to encapsulate specific areas of the API. For example, the `Veh
5254

5355
```kotlin
5456
val api = Api.create()
55-
val response: List<Vehicle> = api.vehicles.all("your-session-token", "organisation-id", VehicleIncludes.SpecificationModel)
57+
val response: List<Vehicle> = api.vehicles.all("organisation-id", VehicleRequestParameters(
58+
includes = listOf(
59+
VehicleIncludes.Specification
60+
)
61+
))
5662
```
5763

5864
## Examples of Usage
5965

6066
```kotlin
6167
Config.environment = Environment.STAGING
62-
val api = Api.create()
63-
64-
api.sessionToken = "sess-123"
65-
val response = api.vehicles.all("org-123", VehicleIncludes.SpecificationModel)
66-
println(response.size)
67-
```
68-
69-
```kotlin
70-
Config.environment = Environment.STAGING
71-
val api = Api.create()
68+
Api.sessionToken = "" // Replace with an actual session token
7269

73-
with(api.vehicles) {
74-
sessionToken = "sess-123"
75-
val response = all("org-123", VehicleIncludes.SpecificationModel)
70+
runBlocking {
71+
val response = Api.vehicles.all("123")
7672
println(response.size)
7773
}
7874
```
Lines changed: 6 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,15 @@
11
package com.ctrlhub.core
22

33
import com.ctrlhub.core.http.KtorClientFactory
4-
import io.ktor.client.HttpClient
5-
import io.ktor.client.plugins.UserAgent
6-
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
7-
import io.ktor.client.plugins.defaultRequest
8-
import io.ktor.http.HttpHeaders
9-
import io.ktor.serialization.kotlinx.json.json
10-
import io.ktor.util.appendIfNameAbsent
11-
import kotlinx.serialization.json.Json
4+
import io.ktor.client.*
125

136
/**
14-
* The "facade" class through which interaction with the API occurs.
7+
* The facade object through which interaction with the API occurs.
158
*/
16-
class Api private constructor(httpClient: HttpClient) {
17-
companion object {
18-
/**
19-
* Creates a new Api instance with a default client
20-
*/
21-
fun create(): Api {
22-
val httpClient = KtorClientFactory.create()
23-
return Api(configureHttpClient(httpClient, Config.apiBaseUrl))
24-
}
9+
object Api {
10+
var sessionToken: String? = null
2511

26-
/**
27-
* Creates a new Api client, with a given HttpClient instance.
28-
* Default config will be applied to this client
29-
*
30-
* @param httpClient HttpClient A ktor client instance
31-
*/
32-
fun create(httpClient: HttpClient): Api {
33-
return Api(httpClient)
34-
}
35-
36-
private fun configureHttpClient(baseClient: HttpClient, baseUrl: String? = null): HttpClient {
37-
return baseClient.config {
38-
baseUrl?.let {
39-
defaultRequest {
40-
url(baseUrl)
41-
}
42-
}
43-
defaultRequest {
44-
headers.appendIfNameAbsent(HttpHeaders.ContentType, "application/json")
45-
}
46-
expectSuccess = true
47-
install(ContentNegotiation) {
48-
json(Json { ignoreUnknownKeys = true })
49-
}
50-
install(UserAgent) {
51-
agent = Config.userAgent
52-
}
53-
}
54-
}
12+
val httpClient: HttpClient by lazy {
13+
KtorClientFactory.create(sessionToken)
5514
}
5615
}

src/main/kotlin/com/ctrlhub/core/api/ApiException.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import io.ktor.client.statement.*
77
*/
88
open class ApiException(message: String, e: Throwable) : Exception(message, e)
99

10+
/**
11+
* Represents an authorized error that occurs when interacting with the API.
12+
*/
13+
class UnauthorizedException(message: String, val response: HttpResponse, e: Throwable) : ApiException(message, e)
14+
1015
/**
1116
* Represents a client based exception that occurred when interacting with the API.
1217
* This is usually the result of a non-200 HTTP response code.
@@ -15,6 +20,4 @@ class ApiClientException(message: String, val response: HttpResponse, e: Throwab
1520
fun statusCode(): Int {
1621
return response.status.value
1722
}
18-
}
19-
20-
class MissingSessionTokenException : Exception("No session token provided")
23+
}
Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
package com.ctrlhub.core.assets.vehicles
22

3-
import com.ctrlhub.core.Config
4-
import com.ctrlhub.core.api.ApiClientException
5-
import com.ctrlhub.core.api.ApiException
3+
import com.ctrlhub.core.Api
64
import com.ctrlhub.core.assets.vehicles.response.VehicleCategory
75
import com.ctrlhub.core.router.Router
86
import com.ctrlhub.core.router.request.RequestParameters
9-
import com.github.jasminb.jsonapi.ResourceConverter
10-
import com.github.jasminb.jsonapi.SerializationFeature
11-
import io.ktor.client.HttpClient
12-
import io.ktor.client.call.body
13-
import io.ktor.client.plugins.ClientRequestException
7+
import io.ktor.client.*
148

15-
class VehicleCategoriesRouter(httpClient: HttpClient) : Router(httpClient, requiresAuthentication = true) {
16-
private val endpoint = "${Config.apiBaseUrl}/v3/assets/vehicles/categories"
9+
class VehicleCategoriesRouter(httpClient: HttpClient) : Router(httpClient) {
10+
private val endpoint = "/v3/assets/vehicles/categories"
1711

1812
/**
1913
* Retrieve all vehicle categories
@@ -23,22 +17,9 @@ class VehicleCategoriesRouter(httpClient: HttpClient) : Router(httpClient, requi
2317
* @return A list of vehicle categories
2418
*/
2519
suspend fun all(requestParameters: RequestParameters = RequestParameters()): List<VehicleCategory> {
26-
return try {
27-
val rawResponse = performGet(endpoint, requestParameters.toMap())
28-
val resourceConverter = ResourceConverter(VehicleCategory::class.java).apply {
29-
enableSerializationOption(SerializationFeature.INCLUDE_RELATIONSHIP_ATTRIBUTES)
30-
}
31-
32-
val jsonApiResponse = resourceConverter.readDocumentCollection<VehicleCategory>(
33-
rawResponse.body<ByteArray>(),
34-
VehicleCategory::class.java
35-
)
36-
37-
jsonApiResponse.get()!!
38-
} catch (e: ClientRequestException) {
39-
throw ApiClientException("All vehicle categories request failed", e.response, e)
40-
} catch (e: Exception) {
41-
throw ApiException("All vehicle categories request failed", e)
42-
}
20+
return fetchJsonApiResources(endpoint, requestParameters.toMap())
4321
}
44-
}
22+
}
23+
24+
val Api.vehicleCategories: VehicleCategoriesRouter
25+
get() = VehicleCategoriesRouter(httpClient)
Lines changed: 10 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
package com.ctrlhub.core.assets.vehicles
22

3-
import com.ctrlhub.core.Config
4-
import com.ctrlhub.core.api.ApiClientException
5-
import com.ctrlhub.core.api.ApiException
3+
import com.ctrlhub.core.Api
64
import com.ctrlhub.core.assets.vehicles.response.VehicleManufacturer
75
import com.ctrlhub.core.assets.vehicles.response.VehicleModel
86
import com.ctrlhub.core.router.Router
97
import com.ctrlhub.core.router.request.JsonApiIncludes
108
import com.ctrlhub.core.router.request.RequestParameters
11-
import com.github.jasminb.jsonapi.ResourceConverter
12-
import com.github.jasminb.jsonapi.SerializationFeature
13-
import io.ktor.client.HttpClient
14-
import io.ktor.client.call.body
15-
import io.ktor.client.plugins.ClientRequestException
9+
import io.ktor.client.*
1610

1711
enum class VehicleManufacturerIncludes(val value: String) : JsonApiIncludes {
1812
Categories("categories");
@@ -25,8 +19,8 @@ enum class VehicleManufacturerIncludes(val value: String) : JsonApiIncludes {
2519
/**
2620
* A vehicles manufacturer router that deals with the vehicle manufacturers realm of the Ctrl Hub API
2721
*/
28-
class VehicleManufacturersRouter(httpClient: HttpClient) : Router(httpClient, requiresAuthentication = true) {
29-
private val endpoint: String = "${Config.apiBaseUrl}/v3/assets/vehicles/manufacturers"
22+
class VehicleManufacturersRouter(httpClient: HttpClient) : Router(httpClient) {
23+
private val endpoint: String = "/v3/assets/vehicles/manufacturers"
3024

3125
/**
3226
* Retrieve all vehicle manufacturers
@@ -36,42 +30,13 @@ class VehicleManufacturersRouter(httpClient: HttpClient) : Router(httpClient, re
3630
* @return A list of vehicle manufacturers
3731
*/
3832
suspend fun all(requestParameters: RequestParameters = RequestParameters()): List<VehicleManufacturer> {
39-
return try {
40-
val rawResponse = performGet(endpoint, requestParameters.toMap())
41-
val resourceConverter = ResourceConverter(VehicleManufacturer::class.java).apply {
42-
enableSerializationOption(SerializationFeature.INCLUDE_RELATIONSHIP_ATTRIBUTES)
43-
}
44-
45-
val jsonApiResponse = resourceConverter.readDocumentCollection<VehicleManufacturer>(
46-
rawResponse.body<ByteArray>(),
47-
VehicleManufacturer::class.java
48-
)
49-
50-
jsonApiResponse.get()!!
51-
} catch (e: ClientRequestException) {
52-
throw ApiClientException("All vehicle manufacturers request failed", e.response, e)
53-
} catch (e: Exception) {
54-
throw ApiException("All vehicle manufacturers request failed", e)
55-
}
33+
return fetchJsonApiResources(endpoint, requestParameters.toMap())
5634
}
5735

5836
suspend fun models(manufacturerId: String, requestParameters: RequestParameters = RequestParameters()): List<VehicleModel> {
59-
return try {
60-
val rawResponse = performGet("$endpoint/$manufacturerId/models", requestParameters.toMap())
61-
val resourceConverter = ResourceConverter(VehicleModel::class.java).apply {
62-
enableSerializationOption(SerializationFeature.INCLUDE_RELATIONSHIP_ATTRIBUTES)
63-
}
64-
65-
val jsonApiResponse = resourceConverter.readDocumentCollection<VehicleModel>(
66-
rawResponse.body<ByteArray>(),
67-
VehicleModel::class.java
68-
)
69-
70-
jsonApiResponse.get()!!
71-
} catch (e: ClientRequestException) {
72-
throw ApiClientException("All vehicle models request failed", e.response, e)
73-
} catch (e: Exception) {
74-
throw ApiException("All vehicle models request failed", e)
75-
}
37+
return fetchJsonApiResources("$endpoint/$manufacturerId/models", requestParameters.toMap())
7638
}
77-
}
39+
}
40+
41+
val Api.vehicleManufacturers: VehicleManufacturersRouter
42+
get() = VehicleManufacturersRouter(httpClient)
Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
package com.ctrlhub.core.assets.vehicles
22

33
import com.ctrlhub.core.Api
4-
import com.ctrlhub.core.api.ApiClientException
5-
import com.ctrlhub.core.api.ApiException
4+
import com.ctrlhub.core.assets.vehicles.VehicleRequestParameters
65
import com.ctrlhub.core.assets.vehicles.response.Vehicle
7-
import com.ctrlhub.core.http.KtorClientFactory
86
import com.ctrlhub.core.router.Router
7+
import com.ctrlhub.core.router.request.FilterOption
98
import com.ctrlhub.core.router.request.JsonApiIncludes
109
import com.ctrlhub.core.router.request.RequestParameters
11-
import com.github.jasminb.jsonapi.ResourceConverter
12-
import com.github.jasminb.jsonapi.SerializationFeature
10+
import com.ctrlhub.core.router.request.RequestParametersWithIncludes
1311
import io.ktor.client.*
14-
import io.ktor.client.call.*
15-
import io.ktor.client.plugins.*
1612

1713
/**
1814
* Represents JSONAPI includes that are associated for vehicles
@@ -33,10 +29,15 @@ enum class VehicleIncludes(val value: String) : JsonApiIncludes {
3329
}
3430
}
3531

32+
class VehicleRequestParameters(
33+
filterOptions: List<FilterOption> = emptyList(),
34+
includes: List<VehicleIncludes> = emptyList()
35+
) : RequestParametersWithIncludes<VehicleIncludes>(filterOptions, includes)
36+
3637
/**
3738
* A vehicles router that deals with the vehicles realm of the Ctrl Hub API
3839
*/
39-
class VehiclesRouter(httpClient: HttpClient) : Router(httpClient, requiresAuthentication = true) {
40+
class VehiclesRouter(httpClient: HttpClient) : Router(httpClient) {
4041

4142
/**
4243
* Request all vehicles
@@ -48,28 +49,11 @@ class VehiclesRouter(httpClient: HttpClient) : Router(httpClient, requiresAuthen
4849
*/
4950
suspend fun all(
5051
organisationId: String,
51-
requestParameters: RequestParameters = RequestParameters()
52+
requestParameters: VehicleRequestParameters = VehicleRequestParameters()
5253
): List<Vehicle> {
53-
return try {
54-
val rawResponse =
55-
performGet("/v3/orgs/$organisationId/assets/vehicles", requestParameters.toMap())
56-
val resourceConverter = ResourceConverter(Vehicle::class.java).apply {
57-
enableSerializationOption(SerializationFeature.INCLUDE_RELATIONSHIP_ATTRIBUTES)
58-
}
59-
60-
val jsonApiResponse = resourceConverter.readDocumentCollection<Vehicle>(
61-
rawResponse.body<ByteArray>(),
62-
Vehicle::class.java
63-
)
64-
65-
jsonApiResponse.get()!!
66-
} catch (e: ClientRequestException) {
67-
throw ApiClientException("All vehicles request failed", e.response, e)
68-
} catch (e: Exception) {
69-
throw ApiException("All vehicles request failed", e)
70-
}
54+
return fetchJsonApiResources("/v3/orgs/$organisationId/assets/vehicles", requestParameters.toMap())
7155
}
7256
}
7357

7458
val Api.vehicles: VehiclesRouter
75-
get() = VehiclesRouter(KtorClientFactory.create())
59+
get() = VehiclesRouter(httpClient)

src/main/kotlin/com/ctrlhub/core/auth/AuthRouter.kt

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,17 @@ import com.ctrlhub.core.auth.payload.LoginPayload
88
import com.ctrlhub.core.auth.payload.LogoutPayload
99
import com.ctrlhub.core.auth.response.AuthFlowResponse
1010
import com.ctrlhub.core.auth.response.CompleteResponse
11-
import com.ctrlhub.core.http.KtorClientFactory
1211
import com.ctrlhub.core.router.Router
13-
import io.ktor.client.HttpClient
12+
import io.ktor.client.*
1413
import io.ktor.client.call.*
1514
import io.ktor.client.plugins.*
16-
import io.ktor.client.request.delete
17-
import io.ktor.client.request.get
18-
import io.ktor.client.request.header
19-
import io.ktor.client.request.post
20-
import io.ktor.client.request.setBody
15+
import io.ktor.client.request.*
2116
import io.ktor.http.*
22-
import kotlinx.serialization.encodeToString
23-
import kotlinx.serialization.json.Json
2417

2518
/**
2619
* A router that deals with authenticating through the Ctrl Hub API
2720
*/
28-
class AuthRouter(httpClient: HttpClient) : Router(httpClient = httpClient, requiresAuthentication = false) {
21+
class AuthRouter(httpClient: HttpClient) : Router(httpClient = httpClient) {
2922

3023
/**
3124
* Initiates a flow for authentication. This needs to be called first, before completing
@@ -95,7 +88,7 @@ class AuthRouter(httpClient: HttpClient) : Router(httpClient = httpClient, requi
9588
* @return true if the session could be invalidated successfully, false if not
9689
* @throws ApiException If an exception occurred whilst attempting to invalidate a session
9790
*/
98-
suspend fun logout(): Boolean {
91+
suspend fun logout(sessionToken: String): Boolean {
9992
return try {
10093
val statusCode = performDelete("${Config.authBaseUrl}/self-service/logout/api", body = LogoutPayload(
10194
sessionToken = sessionToken.toString()
@@ -111,4 +104,4 @@ class AuthRouter(httpClient: HttpClient) : Router(httpClient = httpClient, requi
111104
}
112105

113106
val Api.auth: AuthRouter
114-
get() = AuthRouter(KtorClientFactory.create())
107+
get() = AuthRouter(httpClient)

0 commit comments

Comments
 (0)