-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor Kotlin RTL #384
base: main
Are you sure you want to change the base?
Refactor Kotlin RTL #384
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,17 +56,30 @@ Verify authentication works and that API calls will succeed with code similar to | |
```kotlin | ||
import com.looker.rtl.ApiSettings; | ||
import com.looker.rtl.AuthSession; | ||
import com.looker.rtl.SdkResult; | ||
import com.looker.rtl.ok; | ||
import com.looker.sdk.LookerSDK; | ||
|
||
val localIni = "./looker.ini" | ||
val settings = ApiSettings.fromIniFile(localIni, "Looker") | ||
val session = AuthSession(settings) | ||
val sdk = LookerSDK(session) | ||
// Verify minimal SDK call works | ||
val me = sdk.ok<User>(sdk.me()) | ||
val me = sdk.me().ok() | ||
|
||
/// continue making SDK calls | ||
val users = sdk.ok<Array<User>>(sdk.all_users()) | ||
val result = sdk.all_users() | ||
when (result) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I extended the example here to show what you can do if you want to check for errors. |
||
is SdkResult.SuccessResponse<List<User>> -> { | ||
result.body.forEach { user -> print(user.name) } | ||
} | ||
is SdkResult.FailureResponse<com.looker.sdk.Error> -> { | ||
log(result.body.message) | ||
} | ||
is SdkResult.Error -> { | ||
log(result.error.message) | ||
} | ||
} | ||
``` | ||
|
||
### More examples | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -111,15 +111,6 @@ open class AuthSession( | |
return false | ||
} | ||
|
||
fun <T> ok(response: SDKResponse): T { | ||
@Suppress("UNCHECKED_CAST") | ||
when (response) { | ||
is SDKResponse.SDKErrorResponse<*> -> throw Error(response.value.toString()) | ||
is SDKResponse.SDKSuccessResponse<*> -> return response.value as T | ||
else -> throw Error("Fail!!") | ||
} | ||
} | ||
|
||
private fun sudoLogout(): Boolean { | ||
var result = false | ||
if (isSudo()) { | ||
|
@@ -148,20 +139,17 @@ open class AuthSession( | |
append(client_secret, clientSecret) | ||
} | ||
) | ||
val token = ok<AuthToken>( | ||
transport.request<AuthToken>( | ||
HttpMethod.POST, | ||
"$apiPath/login", | ||
mapOf(), | ||
body | ||
) | ||
) | ||
authToken = token | ||
authToken = transport.request<AuthToken, Any?>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
HttpMethod.POST, | ||
"$apiPath/login", | ||
mapOf(), | ||
body | ||
).ok() | ||
} | ||
|
||
if (sudoId.isNotBlank()) { | ||
val token = activeToken() | ||
val sudoToken = transport.request<AuthToken>( | ||
val sudoToken = transport.request<AuthToken, Any?>( | ||
HttpMethod.POST, | ||
"/login/$newId" | ||
) { requestSettings -> | ||
|
@@ -171,26 +159,23 @@ open class AuthSession( | |
} | ||
requestSettings.copy(headers = headers) | ||
} | ||
this.sudoToken = ok(sudoToken) | ||
this.sudoToken = sudoToken.ok() | ||
} | ||
return activeToken() | ||
} | ||
|
||
private fun doLogout(): Boolean { | ||
val token = activeToken() | ||
val resp = transport.request<String>(HttpMethod.DELETE, "/logout") { | ||
val resp = transport.request<String, Any?>(HttpMethod.DELETE, "/logout") { | ||
val headers = it.headers.toMutableMap() | ||
if (token.accessToken.isNotBlank()) { | ||
headers["Authorization"] = "Bearer ${token.accessToken}" | ||
} | ||
it.copy(headers = headers) | ||
} | ||
|
||
val success = when (resp) { | ||
is SDKResponse.SDKSuccessResponse<*> -> true | ||
is SDKResponse.SDKErrorResponse<*> -> false | ||
else -> false | ||
} | ||
val success = resp.success | ||
|
||
if (sudoId.isNotBlank()) { | ||
sudoId = "" | ||
sudoToken.reset() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package com.looker.rtl | ||
|
||
import io.ktor.client.call.receive | ||
import io.ktor.client.response.HttpResponse | ||
import io.ktor.http.isSuccess | ||
import kotlinx.coroutines.runBlocking | ||
|
||
class FailureResponseError(val result: SdkResult<*, *>) : Exception() | ||
|
||
interface SdkResponse<T> { | ||
val success: Boolean | ||
val statusCode: Int | ||
val method: HttpMethod | ||
val path: String | ||
fun body(): T | ||
} | ||
|
||
sealed class SdkResult<out TSuccess, out TFailure> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add some more comments to these new classes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know you're just augmenting the existing pattern of an sdk method returns a "meta" object that you can get the actual SDK response type from (on success) with My question is at a higher design level for you and @jkaster - searching through some pretty popular python sdks (github, azure, google cloud sdks) - the idiomatic approach for these python sdks is for a method to behave as if the For python I'd actually propose adding a method on the SDK response types that exposes the success/status_code/method/path/raw_body metadata (and for the error case throwing an exception that has this same metadata access method). Is it ok from our design perspective that the python sdk behaves so differently? We could make it behave like the others but then I think we lose the idiomatic nature that other popular python sdk libraries are following. thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's definitely simpler if the sdk response is always just TSuccess or an exception. I guess since my motivation for this was better error handling, we could limit the extra functionality to just the error path. That would limit the client's ability to introspect on the response in the success case, but maybe we don't care about that. Also, I remember @jkaster talking about how he didn't want to throw in the failure/error case until you actually went to get the value. Something about working better in an async flow, so maybe we should look at what node or something else natively async does here. I'd be up for a quick group chat about this topic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. happy to participate - I'll let you and our fearless leader @jkaster coordinate :-) |
||
abstract val success: Boolean | ||
abstract val method: HttpMethod | ||
abstract val path: String | ||
|
||
data class SuccessResponse<TSuccess>( | ||
val response: HttpResponse, | ||
override val method: HttpMethod, | ||
override val path: String, | ||
private val body: TSuccess | ||
) : SdkResponse<TSuccess>, SdkResult<TSuccess, Nothing>() { | ||
override val success: Boolean = true | ||
override val statusCode: Int = response.status.value | ||
|
||
override fun body(): TSuccess = body | ||
|
||
inline fun <reified T> bodyAs(): T { | ||
return runBlocking { response.receive<T>() } | ||
} | ||
} | ||
|
||
data class FailureResponse<TFailure>( | ||
val response: HttpResponse, | ||
override val method: HttpMethod, | ||
override val path: String, | ||
private val body: TFailure | ||
) : SdkResponse<TFailure>, SdkResult<Nothing, TFailure>() { | ||
override val success: Boolean = false | ||
override val statusCode: Int = response.status.value | ||
|
||
override fun body(): TFailure = body | ||
|
||
inline fun <reified T> bodyAs(): T { | ||
return runBlocking { response.receive<T>() } | ||
} | ||
} | ||
|
||
data class Error( | ||
val error: Throwable, | ||
override val method: HttpMethod, | ||
override val path: String | ||
) : SdkResult<Nothing, Nothing>() { | ||
override val success: Boolean = false | ||
} | ||
|
||
companion object { | ||
inline fun <reified TSuccess, reified TFailure> response( | ||
response: HttpResponse, | ||
method: HttpMethod, | ||
path: String | ||
): SdkResult<TSuccess, TFailure> { | ||
try { | ||
if (response.status.isSuccess()) { | ||
val body = runBlocking { response.receive<TSuccess>() } | ||
return SuccessResponse<TSuccess>(response, method, path, body) | ||
} else { | ||
val body = runBlocking { response.receive<TFailure>() } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pondering this some more, I'm wondering if the FailureResponse case should actually defer on receiving the data until requested. The issue I'm running into while thinking this through is that I want to be able to deal with a single API being able to return multiple different types of error response. In that case, we're going to have to do some preliminary parsing or some try/catch(ParseError) for the different types. With ktor, you can only do I'm thinking that maybe we do Looking at the error responses and some of the libraries like Jackson or kotlinx.serialization, it would be nice to have a discriminator field to use to select the type to parse into, but our API doesn't really provide that. Just brain dumping into this comment for further discussion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well, it will be a comfort python isn't the only language with a separate [de]serialization layer. I always thought it was unfair that the other languages just somehow got their models/types hydrated for free from the transport. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated feedback
|
||
return FailureResponse<TFailure>(response, method, path, body) | ||
} | ||
} catch (ex: Exception) { | ||
return error(ex, method, path) | ||
} | ||
} | ||
|
||
fun <TSuccess, TFailure> error( | ||
error: Throwable, | ||
method: HttpMethod, | ||
path: String | ||
): SdkResult<TSuccess, TFailure> { | ||
return SdkResult.Error(error, method, path) | ||
} | ||
} | ||
} | ||
|
||
fun <TSuccess> SdkResult<TSuccess, *>.ok(): TSuccess { | ||
return when (this) { | ||
is SdkResult.SuccessResponse<TSuccess> -> { | ||
this.body() | ||
} | ||
is SdkResult.FailureResponse<*> -> { | ||
throw FailureResponseError(this) | ||
} | ||
is SdkResult.Error -> { | ||
throw this.error | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept the
ok()
concept. Presumably this is used in the other SDKs as well for a short circuit to a more robust analysis.