Skip to content

Commit 584e5c5

Browse files
committed
task: refactor common router logic
1 parent 17a1fd8 commit 584e5c5

File tree

12 files changed

+145
-82
lines changed

12 files changed

+145
-82
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,24 @@ val api = Api.create()
5555
val response: List<Vehicle> = api.vehicles.all("your-session-token", "organisation-id", VehicleIncludes.SpecificationModel)
5656
```
5757

58-
## Example Usage
58+
## Examples of Usage
5959

6060
```kotlin
6161
Config.environment = Environment.STAGING
6262
val api = Api.create()
6363

64-
val response = api.vehicles.all("sess-123", "org-123", VehicleIncludes.SpecificationModel)
64+
api.sessionToken = "sess-123"
65+
val response = api.vehicles.all("org-123", VehicleIncludes.SpecificationModel)
6566
println(response.size)
67+
```
68+
69+
```kotlin
70+
Config.environment = Environment.STAGING
71+
val api = Api.create()
72+
73+
with(api.vehicles) {
74+
sessionToken = "sess-123"
75+
val response = all("org-123", VehicleIncludes.SpecificationModel)
76+
println(response.size)
77+
}
6678
```

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ class ApiClientException(message: String, val response: HttpResponse, e: Throwab
1515
fun statusCode(): Int {
1616
return response.status.value
1717
}
18-
}
18+
}
19+
20+
class MissingSessionTokenException : Exception("No session token provided")

src/main/kotlin/com/ctrlhub/core/assets/vehicles/VehiclesRouter.kt

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
package com.ctrlhub.core.assets.vehicles
22

33
import com.ctrlhub.core.Api
4-
import com.ctrlhub.core.Config
54
import com.ctrlhub.core.api.ApiClientException
65
import com.ctrlhub.core.api.ApiException
76
import com.ctrlhub.core.assets.vehicles.response.Vehicle
87
import com.ctrlhub.core.http.KtorClientFactory
98
import com.ctrlhub.core.router.Router
9+
import com.ctrlhub.core.router.request.JsonApiIncludes
1010
import com.github.jasminb.jsonapi.ResourceConverter
1111
import com.github.jasminb.jsonapi.SerializationFeature
12-
import io.ktor.client.HttpClient
13-
import io.ktor.client.call.body
14-
import io.ktor.client.plugins.ClientRequestException
15-
import io.ktor.client.request.get
16-
import io.ktor.client.request.header
17-
import io.ktor.http.headers
12+
import io.ktor.client.*
13+
import io.ktor.client.call.*
14+
import io.ktor.client.plugins.*
1815

1916
/**
2017
* Represents JSONAPI includes that are associated for vehicles
2118
*/
22-
enum class VehicleIncludes(val value: String) {
19+
enum class VehicleIncludes(val value: String) : JsonApiIncludes {
2320
Status("status"),
2421
Assignee("assignee"),
2522
Equipment("equipment"),
@@ -28,41 +25,30 @@ enum class VehicleIncludes(val value: String) {
2825
EquipmentModelCategories("equipment.model.categories"),
2926
Specification("specification"),
3027
SpecificationModel("specification.model"),
31-
SpecificationModelManufacturer("specification.model.manufacturer")
28+
SpecificationModelManufacturer("specification.model.manufacturer");
29+
30+
override fun value(): String {
31+
return value
32+
}
3233
}
3334

3435
/**
3536
* A vehicles router that deals with the vehicles realm of the Ctrl Hub API
3637
*/
37-
class VehiclesRouter(httpClient: HttpClient) : Router(httpClient) {
38+
class VehiclesRouter(httpClient: HttpClient) : Router(httpClient, requiresAuthentication = true) {
3839

3940
/**
4041
* Request all vehicles
4142
*
42-
* @param sessionToken String A valid session token gained after authentication
4343
* @param organisationId String The organisation ID to retrieve all vehicles for
4444
* @param includes A variable list of any includes, to return in the response. For example, VehicleIncludes.Specification will provide additional Specification attributes
4545
*
4646
* @return A list of all vehicles
4747
*/
48-
suspend fun all(sessionToken: String, organisationId: String, vararg includes: VehicleIncludes): List<Vehicle> {
49-
val includesQuery = if (includes.isNotEmpty()) {
50-
"?include=${includes.joinToString(",") { it.value }}"
51-
} else {
52-
""
53-
}
54-
55-
return fetchAllVehicles(sessionToken, organisationId, includesQuery)
56-
}
57-
58-
private suspend fun fetchAllVehicles(sessionToken: String, organisationId: String, includesQuery: String): List<Vehicle> {
48+
suspend fun all(organisationId: String, vararg includes: VehicleIncludes = emptyArray()): List<Vehicle> {
5949
return try {
60-
val rawResponse = httpClient.get("${Config.apiBaseUrl}/v3/orgs/$organisationId/assets/vehicles$includesQuery") {
61-
headers {
62-
header("X-Session-Token", sessionToken)
63-
}
64-
}
65-
50+
val includesQuery = buildIncludesQueryString(*includes)
51+
val rawResponse = performGet("/v3/orgs/$organisationId/assets/vehicles" + (if (includesQuery.isNotEmpty()) "?$includesQuery" else ""))
6652
val resourceConverter = ResourceConverter(Vehicle::class.java).apply {
6753
enableSerializationOption(SerializationFeature.INCLUDE_RELATIONSHIP_ATTRIBUTES)
6854
}

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

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import kotlinx.serialization.json.Json
2525
/**
2626
* A router that deals with authenticating through the Ctrl Hub API
2727
*/
28-
class AuthRouter(httpClient: HttpClient) : Router(httpClient = httpClient) {
28+
class AuthRouter(httpClient: HttpClient) : Router(httpClient = httpClient, requiresAuthentication = false) {
2929

3030
/**
3131
* Initiates a flow for authentication. This needs to be called first, before completing
@@ -48,18 +48,12 @@ class AuthRouter(httpClient: HttpClient) : Router(httpClient = httpClient) {
4848
/**
4949
* Provides a mechanism for refreshing a session
5050
*
51-
* @param sessionToken String A valid session token obtained via authentication
52-
*
5351
* @throws ApiClientException when a client based exception occurs, usually as a result of a non-200 HTTP response code
5452
* @throws ApiException when another type of exception occurs
5553
*/
56-
suspend fun refresh(sessionToken: String): AuthFlowResponse {
54+
suspend fun refresh(): AuthFlowResponse {
5755
return try {
58-
httpClient.get("${Config.authBaseUrl}/self-service/login/api?refresh=true") {
59-
headers {
60-
header("X-Session-Token", sessionToken)
61-
}
62-
}.body()
56+
performGet("${Config.authBaseUrl}/self-service/login/api?refresh=true").body()
6357
} catch (e: ClientRequestException) {
6458
throw ApiClientException("Failed to initiate auth", e.response, e)
6559
} catch (e: Exception) {
@@ -80,9 +74,7 @@ class AuthRouter(httpClient: HttpClient) : Router(httpClient = httpClient) {
8074
*/
8175
suspend fun complete(flowId: String, payload: LoginPayload): CompleteResponse {
8276
return try {
83-
httpClient.post("${Config.authBaseUrl}/self-service/login?flow=$flowId") {
84-
setBody(Json.encodeToString(payload))
85-
}.body()
77+
performPost("${Config.authBaseUrl}/self-service/login?flow=$flowId", payload).body()
8678
} catch (e: ClientRequestException) {
8779
if (e.response.status == HttpStatusCode.BadRequest) {
8880
val bodyAsString: String = e.response.body()
@@ -100,18 +92,14 @@ class AuthRouter(httpClient: HttpClient) : Router(httpClient = httpClient) {
10092
/**
10193
* Invalidates a session
10294
*
103-
* @param sessionToken String A session token that will identify the session to be invalidated
104-
*
105-
* @return true if the session could be invalidated successfuly, false if not
95+
* @return true if the session could be invalidated successfully, false if not
10696
* @throws ApiException If an exception occurred whilst attempting to invalidate a session
10797
*/
108-
suspend fun logout(sessionToken: String): Boolean {
98+
suspend fun logout(): Boolean {
10999
return try {
110-
val statusCode = httpClient.delete("${Config.authBaseUrl}/self-service/logout/api") {
111-
setBody(Json.encodeToString(LogoutPayload(
112-
sessionToken = sessionToken
113-
)))
114-
}.status
100+
val statusCode = performDelete("${Config.authBaseUrl}/self-service/logout/api", body = LogoutPayload(
101+
sessionToken = sessionToken.toString()
102+
)).status
115103

116104
statusCode == HttpStatusCode.NoContent
117105
} catch (e: ClientRequestException) {

src/main/kotlin/com/ctrlhub/core/governance/OrganisationsRouter.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,16 @@ import com.github.jasminb.jsonapi.ResourceConverter
1111
import io.ktor.client.HttpClient
1212
import io.ktor.client.call.body
1313
import io.ktor.client.plugins.ClientRequestException
14-
import io.ktor.client.request.get
15-
import io.ktor.client.request.header
16-
import io.ktor.http.headers
1714

1815
/**
1916
* An organisation router that deals with the organisations realm of the Ctrl Hub API
2017
*/
21-
class OrganisationsRouter(httpClient: HttpClient) : Router(httpClient) {
22-
suspend fun all(sessionToken: String): List<Organisation> {
23-
return try {
24-
val rawResponse = httpClient.get("${Config.apiBaseUrl}/v3/orgs") {
25-
headers {
26-
header("X-Session-Token", sessionToken)
27-
}
28-
}
18+
class OrganisationsRouter(httpClient: HttpClient) : Router(httpClient, requiresAuthentication = true) {
19+
private val endpoint = "${Config.apiBaseUrl}/v3/orgs"
2920

21+
suspend fun all(): List<Organisation> {
22+
return try {
23+
val rawResponse = performGet(endpoint)
3024
val resourceConverter = ResourceConverter(Organisation::class.java)
3125
val jsonApiResponse = resourceConverter.readDocumentCollection<Organisation>(
3226
(rawResponse.body<ByteArray>()),

src/main/kotlin/com/ctrlhub/core/iam/IamRouter.kt

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,16 @@ import io.ktor.http.headers
1818
/**
1919
* A router that deals with the Iam realm of the Ctrl Hub API
2020
*/
21-
class IamRouter(httpClient: HttpClient) : Router(httpClient) {
21+
class IamRouter(httpClient: HttpClient) : Router(httpClient, requiresAuthentication = true) {
2222

2323
/**
2424
* Returns information about the currently authenticated user, based on a session token
2525
*
26-
* @param sessionToken String A valid session token obtained via authentication
27-
*
2826
* @return A user object providing information about a currently authenticated user
2927
*/
30-
suspend fun whoami(sessionToken: String): User {
28+
suspend fun whoami(): User {
3129
return try {
32-
val rawResponse = httpClient.get("${Config.apiBaseUrl}/v3/iam/whoami") {
33-
headers {
34-
header("X-Session-Token", sessionToken)
35-
}
36-
}
37-
30+
val rawResponse = performGet("${Config.apiBaseUrl}/v3/iam/whoami")
3831
val resourceConverter = ResourceConverter(User::class.java)
3932
val jsonApiResponse = resourceConverter.readDocument<User>(rawResponse.body<ByteArray>(), User::class.java)
4033

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,80 @@
11
package com.ctrlhub.core.router
22

3+
import com.ctrlhub.core.api.MissingSessionTokenException
4+
import com.ctrlhub.core.router.request.JsonApiIncludes
35
import io.ktor.client.HttpClient
6+
import io.ktor.client.request.delete
7+
import io.ktor.client.request.get
8+
import io.ktor.client.request.header
9+
import io.ktor.client.request.post
10+
import io.ktor.client.request.setBody
11+
import io.ktor.client.statement.HttpResponse
12+
import io.ktor.http.ContentType
13+
import io.ktor.http.contentType
14+
import io.ktor.http.headers
415

5-
abstract class Router(val httpClient: HttpClient)
16+
abstract class Router(val httpClient: HttpClient, val requiresAuthentication: Boolean) {
17+
var sessionToken: String? = null
18+
19+
protected fun buildIncludesQueryString(vararg includes: JsonApiIncludes): String {
20+
if (includes.isEmpty()) {
21+
return ""
22+
}
23+
return "include=" + includes.joinToString(",") { it.value().lowercase() }
24+
}
25+
26+
protected suspend fun performGet(endpoint: String): HttpResponse {
27+
if (requiresAuthentication && sessionToken.isNullOrEmpty()) {
28+
throw MissingSessionTokenException()
29+
}
30+
31+
return httpClient.get(endpoint) {
32+
if (requiresAuthentication && !sessionToken.isNullOrEmpty()) headers {
33+
header("X-Session-Token", sessionToken!!)
34+
}
35+
}
36+
}
37+
38+
protected suspend inline fun <reified T> performPost(endpoint: String, body: T): HttpResponse {
39+
if (requiresAuthentication && sessionToken.isNullOrEmpty()) {
40+
throw MissingSessionTokenException()
41+
}
42+
return httpClient.post(endpoint) {
43+
contentType(ContentType.Application.Json)
44+
setBody(body)
45+
46+
if (requiresAuthentication && !sessionToken.isNullOrEmpty()) headers {
47+
header("X-Session-Token", sessionToken!!)
48+
}
49+
}
50+
}
51+
52+
protected suspend inline fun <reified T> performDelete(endpoint: String, body: T): HttpResponse {
53+
if (requiresAuthentication && sessionToken == null) {
54+
throw MissingSessionTokenException()
55+
}
56+
57+
return httpClient.delete(endpoint) {
58+
contentType(ContentType.Application.Json)
59+
setBody(body)
60+
61+
if (requiresAuthentication && !sessionToken.isNullOrEmpty()) headers {
62+
header("X-Session-Token", sessionToken!!)
63+
}
64+
}
65+
}
66+
67+
protected suspend inline fun <reified T> performDelete(endpoint: String): HttpResponse {
68+
if (requiresAuthentication && sessionToken == null) {
69+
throw MissingSessionTokenException()
70+
}
71+
72+
return httpClient.delete(endpoint) {
73+
contentType(ContentType.Application.Json)
74+
75+
if (requiresAuthentication && !sessionToken.isNullOrEmpty()) headers {
76+
header("X-Session-Token", sessionToken!!)
77+
}
78+
}
79+
}
80+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.ctrlhub.core.router.request
2+
3+
/**
4+
* A marker interface for includes options that can be specified
5+
* when making a request
6+
*/
7+
interface JsonApiIncludes {
8+
fun value(): String
9+
}

src/test/kotlin/com/ctrlhub/core/assets/vehicles/VehiclesRouterTest.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ class VehiclesRouterTest {
3030
}
3131

3232
val vehiclesRouter = VehiclesRouter(httpClient = HttpClient(mockEngine).configureForTest())
33+
vehiclesRouter.sessionToken = "sess-123"
3334

3435
runBlocking {
35-
val response = vehiclesRouter.all(sessionToken = "test-token", organisationId = "123")
36+
val response = vehiclesRouter.all(organisationId = "123")
3637
assertIs<List<Vehicle>>(response)
3738
assertNotNull(response[0].id)
3839
}
@@ -52,9 +53,10 @@ class VehiclesRouterTest {
5253
}
5354

5455
val vehiclesRouter = VehiclesRouter(httpClient = HttpClient(mockEngine).configureForTest())
56+
vehiclesRouter.sessionToken = "sess-123"
5557

5658
runBlocking {
57-
val response = vehiclesRouter.all(sessionToken = "test-token", organisationId = "123", VehicleIncludes.SpecificationModel)
59+
val response = vehiclesRouter.all(organisationId = "123", VehicleIncludes.SpecificationModel)
5860
assertIs<List<Vehicle>>(response)
5961
val first = response[0]
6062
assertNotNull(first.specification?.model)

src/test/kotlin/com/ctrlhub/core/auth/AuthRouterTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class AuthRouterTest {
105105

106106
runBlocking {
107107
assertFailsWith<ApiException> {
108-
authRouter.refresh("sess-123")
108+
authRouter.refresh()
109109
}
110110
}
111111
}
@@ -123,7 +123,7 @@ class AuthRouterTest {
123123
val authRouter = AuthRouter(httpClient = HttpClient(mockEngine).configureForTest())
124124

125125
runBlocking {
126-
val response = authRouter.refresh("sess-123")
126+
val response = authRouter.refresh()
127127
assertEquals("test-123", response.id)
128128
}
129129
}
@@ -141,7 +141,7 @@ class AuthRouterTest {
141141
val authRouter = AuthRouter(httpClient = HttpClient(mockEngine).configureForTest())
142142

143143
runBlocking {
144-
val result = authRouter.logout("dummy-session-token")
144+
val result = authRouter.logout()
145145
assertTrue(result, "Logout should return true on success")
146146
}
147147
}
@@ -159,7 +159,7 @@ class AuthRouterTest {
159159
val authRouter = AuthRouter(httpClient = HttpClient(mockEngine).configureForTest())
160160

161161
runBlocking {
162-
val result = authRouter.logout("dummy-session-token")
162+
val result = authRouter.logout()
163163
assertFalse(result, "Logout should return false on success")
164164
}
165165
}

0 commit comments

Comments
 (0)