Skip to content

Commit 355e76f

Browse files
authored
Add Geocoding endpoints (#179)
1 parent fc38f1d commit 355e76f

21 files changed

+616
-11
lines changed

library/src/commonMain/kotlin/com/ioki/passenger/api/IokiService.kt

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ import com.ioki.passenger.api.models.ApiFailedPaymentResponse
2727
import com.ioki.passenger.api.models.ApiFareResponse
2828
import com.ioki.passenger.api.models.ApiFirebaseDebugRecordRequest
2929
import com.ioki.passenger.api.models.ApiFirebaseTokenResponse
30+
import com.ioki.passenger.api.models.ApiGeocodingDetailsRequest
31+
import com.ioki.passenger.api.models.ApiGeocodingDetailsResponse
32+
import com.ioki.passenger.api.models.ApiGeocodingSearchRequest
33+
import com.ioki.passenger.api.models.ApiGeocodingSearchResponse
34+
import com.ioki.passenger.api.models.ApiGeocodingSessionRequest
35+
import com.ioki.passenger.api.models.ApiGeocodingSessionResponse
3036
import com.ioki.passenger.api.models.ApiLogPayAccountRequest
3137
import com.ioki.passenger.api.models.ApiLogPayType
3238
import com.ioki.passenger.api.models.ApiLogPayUrlResponse
@@ -145,7 +151,8 @@ public interface IokiService :
145151
PublicTransportService,
146152
TicketingService,
147153
StationsService,
148-
VenuesService
154+
VenuesService,
155+
GeocodingService
149156

150157
public interface PhoneVerificationService {
151158
public suspend fun solveCaptcha(captchaId: String, captchaRequest: ApiCaptchaRequest): ApiResult<Unit>
@@ -388,6 +395,19 @@ public interface PurchaseService {
388395
public suspend fun resettleDebits(request: ApiResettleDebitsRequest): ApiResult<List<ApiPurchaseResponse>>
389396
}
390397

398+
public interface GeocodingService {
399+
public suspend fun getGeocodingSession(request: ApiGeocodingSessionRequest): ApiResult<ApiGeocodingSessionResponse>
400+
public suspend fun expireGeocodingSession(sessionId: String): ApiResult<Unit>
401+
public suspend fun getGeocodingSearch(
402+
sessionId: String,
403+
request: ApiGeocodingSearchRequest,
404+
): ApiResult<ApiGeocodingSearchResponse>
405+
public suspend fun getGeocodingDetails(
406+
sessionId: String,
407+
request: ApiGeocodingDetailsRequest,
408+
): ApiResult<ApiGeocodingDetailsResponse>
409+
}
410+
391411
private class DefaultIokiService(
392412
private val iokiApi: IokiApi,
393413
private val interceptors: Set<ApiErrorInterceptor>,
@@ -832,6 +852,33 @@ private class DefaultIokiService(
832852
getRatingCriteria(rideId = rideId)
833853
}
834854

855+
override suspend fun getGeocodingSession(
856+
request: ApiGeocodingSessionRequest,
857+
): ApiResult<ApiGeocodingSessionResponse> =
858+
apiCall<ApiBody<ApiGeocodingSessionResponse>, ApiGeocodingSessionResponse> {
859+
geocodingSession(body = ApiBody(request))
860+
}
861+
862+
override suspend fun expireGeocodingSession(sessionId: String): ApiResult<Unit> = apiCall<Unit, Unit> {
863+
expireGeocodingSession(id = sessionId)
864+
}
865+
866+
override suspend fun getGeocodingSearch(
867+
sessionId: String,
868+
request: ApiGeocodingSearchRequest,
869+
): ApiResult<ApiGeocodingSearchResponse> =
870+
apiCall<ApiBody<ApiGeocodingSearchResponse>, ApiGeocodingSearchResponse> {
871+
geocodingSearch(id = sessionId, body = ApiBody(request))
872+
}
873+
874+
override suspend fun getGeocodingDetails(
875+
sessionId: String,
876+
request: ApiGeocodingDetailsRequest,
877+
): ApiResult<ApiGeocodingDetailsResponse> =
878+
apiCall<ApiBody<ApiGeocodingDetailsResponse>, ApiGeocodingDetailsResponse> {
879+
geocodingDetails(id = sessionId, body = ApiBody(request))
880+
}
881+
835882
private suspend inline fun <reified R, reified T> apiCall(
836883
crossinline block: suspend IokiApi.() -> HttpResponse,
837884
): ApiResult<T> {

library/src/commonMain/kotlin/com/ioki/passenger/api/internal/api/IokiApi.kt

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import com.ioki.passenger.api.models.ApiDeviceRequest
1515
import com.ioki.passenger.api.models.ApiDoorStateChangeRequest
1616
import com.ioki.passenger.api.models.ApiFailedPaymentRequest
1717
import com.ioki.passenger.api.models.ApiFirebaseDebugRecordRequest
18+
import com.ioki.passenger.api.models.ApiGeocodingDetailsRequest
19+
import com.ioki.passenger.api.models.ApiGeocodingSearchRequest
20+
import com.ioki.passenger.api.models.ApiGeocodingSessionRequest
1821
import com.ioki.passenger.api.models.ApiLogPayAccountRequest
1922
import com.ioki.passenger.api.models.ApiPaymentMethodCreationRequest
2023
import com.ioki.passenger.api.models.ApiPersonalDiscountPurchaseRequest
@@ -501,17 +504,44 @@ internal class IokiApi(
501504
header("Authorization", accessToken)
502505
setBody(body)
503506
}
507+
508+
suspend fun geocodingSession(body: ApiBody<ApiGeocodingSessionRequest>): HttpResponse =
509+
client.post(urlString = "/api/passenger/geocoding/session") {
510+
header("Authorization", accessToken)
511+
setBody(body)
512+
}
513+
514+
suspend fun expireGeocodingSession(id: String): HttpResponse =
515+
client.delete(urlString = "/api/passenger/geocoding/session/$id") {
516+
header("Authorization", accessToken)
517+
}
518+
519+
suspend fun geocodingSearch(id: String, body: ApiBody<ApiGeocodingSearchRequest>): HttpResponse =
520+
client.post(urlString = "/api/passenger/geocoding/session/$id/search") {
521+
header("Authorization", accessToken)
522+
setBody(body)
523+
}
524+
525+
suspend fun geocodingDetails(id: String, body: ApiBody<ApiGeocodingDetailsRequest>): HttpResponse =
526+
client.post(urlString = "/api/passenger/geocoding/session/$id/details") {
527+
header("Authorization", accessToken)
528+
setBody(body)
529+
}
504530
}
505531

506532
private fun ApiPurchaseFilter.toStringValues(): StringValues = StringValues.build {
507-
append("page", page.toString())
508-
perPage?.let { append("per_page", it.toString()) }
509-
since?.let { append("since", it.toString()) }
510-
until?.let { append("until", it.toString()) }
511-
purchasableId?.let { append("purchasable_id", it) }
512-
purchasableType?.let { append("purchasable_type", Json.encodeToString(it).removeSurrounding("\"")) }
513-
state?.let { append("state", Json.encodeToString(it).removeSurrounding("\"")) }
514-
filter?.let { append("filter", Json.encodeToString(it).removeSurrounding("\"")) }
515-
order?.let { append("order", Json.encodeToString(it).removeSurrounding("\"")) }
516-
orderBy?.let { append("order_by", Json.encodeToString(it).removeSurrounding("\"")) }
533+
append(name = "page", value = page.toString())
534+
perPage?.let { append(name = "per_page", value = it.toString()) }
535+
since?.let { append(name = "since", value = it.toString()) }
536+
until?.let { append(name = "until", value = it.toString()) }
537+
purchasableId?.let { append(name = "purchasable_id", value = it) }
538+
purchasableType?.let {
539+
append(name = "purchasable_type", value = Json.encodeToString(value = it).removeSurrounding(delimiter = "\""))
540+
}
541+
state?.let { append(name = "state", value = Json.encodeToString(value = it).removeSurrounding(delimiter = "\"")) }
542+
filter?.let { append(name = "filter", value = Json.encodeToString(value = it).removeSurrounding(delimiter = "\"")) }
543+
order?.let { append(name = "order", value = Json.encodeToString(value = it).removeSurrounding(delimiter = "\"")) }
544+
orderBy?.let {
545+
append(name = "order_by", value = Json.encodeToString(value = it).removeSurrounding(delimiter = "\""))
546+
}
517547
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.ioki.passenger.api.models
2+
3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
6+
public data class ApiGeocodingDetailsRequest(
7+
val id: String,
8+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.ioki.passenger.api.models
2+
3+
import com.ioki.passenger.api.models.ApiGeocodingDetailsResponse.VendorType
4+
import kotlinx.serialization.KSerializer
5+
import kotlinx.serialization.SerialName
6+
import kotlinx.serialization.Serializable
7+
import kotlinx.serialization.builtins.serializer
8+
import kotlinx.serialization.encoding.Decoder
9+
import kotlinx.serialization.encoding.Encoder
10+
11+
@Serializable
12+
public data class ApiGeocodingDetailsResponse(
13+
val lat: Double,
14+
val lng: Double,
15+
val vendor: String,
16+
@SerialName(value = "vendor_id") val vendorId: String,
17+
@SerialName(value = "vendor_type") val vendorType: VendorType,
18+
@SerialName(value = "location_name") val locationName: String?,
19+
@SerialName(value = "formatted_address") val formattedAddress: String?,
20+
@SerialName(value = "street_name") val streetName: String?,
21+
@SerialName(value = "street_number") val streetNumber: String?,
22+
@SerialName(value = "postal_code") val postalCode: String?,
23+
val city: String?,
24+
val county: String?,
25+
val country: String?,
26+
) {
27+
@Serializable(with = VendorTypeSerializer::class)
28+
public enum class VendorType {
29+
@SerialName("station")
30+
STATION,
31+
32+
@SerialName("places")
33+
PLACES,
34+
35+
UNSUPPORTED,
36+
}
37+
}
38+
39+
private object VendorTypeSerializer : KSerializer<VendorType> {
40+
override val descriptor = String.serializer().descriptor
41+
42+
override fun serialize(encoder: Encoder, value: VendorType) {
43+
encoder.encodeString(
44+
when (value) {
45+
VendorType.STATION -> "station"
46+
VendorType.PLACES -> "places"
47+
VendorType.UNSUPPORTED -> "UNSUPPORTED"
48+
},
49+
)
50+
}
51+
52+
override fun deserialize(decoder: Decoder): VendorType {
53+
return when (decoder.decodeString()) {
54+
"station" -> VendorType.STATION
55+
"places" -> VendorType.PLACES
56+
else -> VendorType.UNSUPPORTED
57+
}
58+
}
59+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.ioki.passenger.api.models
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
public data class ApiGeocodingSearchRequest(
8+
val query: String,
9+
@SerialName(value = "product_id") val productId: String,
10+
@SerialName(value = "place_types") val placeTypes: String,
11+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.ioki.passenger.api.models
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
public data class ApiGeocodingSearchResponse(val results: List<SearchResult>) {
8+
@Serializable
9+
public data class SearchResult(
10+
val id: String,
11+
val lat: Double?,
12+
val lng: Double?,
13+
val vendor: String,
14+
@SerialName(value = "vendor_id") val vendorId: String,
15+
@SerialName(value = "location_name") val locationName: String?,
16+
@SerialName(value = "formatted_address") val formattedAddress: String?,
17+
val description: String?,
18+
)
19+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.ioki.passenger.api.models
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
public data class ApiGeocodingSessionRequest(
8+
@SerialName(value = "product_id") val productId: String,
9+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.ioki.passenger.api.models
2+
3+
import kotlin.time.Instant
4+
import kotlinx.serialization.SerialName
5+
import kotlinx.serialization.Serializable
6+
7+
@Serializable
8+
public data class ApiGeocodingSessionResponse(
9+
val id: String,
10+
@SerialName(value = "created_at") val createdAt: Instant,
11+
@SerialName(value = "updated_at") val updatedAt: Instant?,
12+
@SerialName(value = "valid_until") val validUntil: Instant,
13+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.ioki.passenger.api.models
2+
3+
import kotlin.test.Test
4+
5+
internal class ApiGeocodingDetailsRequestTest : IokiApiModelTest() {
6+
@Test
7+
fun serialization() {
8+
testJsonStringCanBeConvertedToModel(
9+
expectedModel = ApiGeocodingDetailsRequest(id = "id_123"),
10+
jsonString = details,
11+
)
12+
}
13+
}
14+
15+
private val details =
16+
"""
17+
{
18+
"id": "id_123"
19+
}
20+
"""
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.ioki.passenger.api.models
2+
3+
import com.ioki.passenger.api.models.ApiGeocodingDetailsResponse.VendorType
4+
import kotlin.test.Test
5+
6+
internal class ApiGeocodingDetailsResponseTest : IokiApiModelTest() {
7+
@Test
8+
fun serialization() {
9+
testJsonStringCanBeConvertedToModel(
10+
expectedModel = ApiGeocodingDetailsResponse(
11+
lat = 50.113695,
12+
lng = 8.678996,
13+
vendorType = VendorType.PLACES,
14+
locationName = "An der Welle",
15+
vendor = "ioki",
16+
vendorId = "vendor_123",
17+
formattedAddress = "An der Welle 3, Frankfurt am Main, Germany",
18+
city = "Frankfurt am Main",
19+
county = "Hessen",
20+
country = "Germany",
21+
postalCode = "60322",
22+
streetName = "An der Welle",
23+
streetNumber = "3",
24+
),
25+
jsonString = details,
26+
)
27+
}
28+
29+
@Test
30+
fun serializationMinimal() {
31+
testJsonStringCanBeConvertedToModel(
32+
expectedModel = ApiGeocodingDetailsResponse(
33+
lat = 50.113695,
34+
lng = 8.678996,
35+
vendor = "ioki",
36+
vendorId = "vendor_123",
37+
vendorType = VendorType.PLACES,
38+
locationName = null,
39+
formattedAddress = null,
40+
city = null,
41+
county = null,
42+
country = null,
43+
postalCode = null,
44+
streetName = null,
45+
streetNumber = null,
46+
),
47+
jsonString = detailsMinimal,
48+
)
49+
}
50+
}
51+
52+
private val details =
53+
"""
54+
{
55+
"lat": 50.113695,
56+
"lng": 8.678996,
57+
"vendor": "ioki",
58+
"vendor_id": "vendor_123",
59+
"vendor_type": "places",
60+
"location_name": "An der Welle",
61+
"formatted_address": "An der Welle 3, Frankfurt am Main, Germany",
62+
"city": "Frankfurt am Main",
63+
"county": "Hessen",
64+
"country": "Germany",
65+
"postal_code": "60322",
66+
"street_name": "An der Welle",
67+
"street_number": "3"
68+
}
69+
"""
70+
71+
private val detailsMinimal =
72+
"""
73+
{
74+
"lat": 50.113695,
75+
"lng": 8.678996,
76+
"vendor": "ioki",
77+
"vendor_id": "vendor_123",
78+
"vendor_type": "places"
79+
}
80+
"""

0 commit comments

Comments
 (0)