Skip to content

Commit

Permalink
feat: capture error details for the Kotlin SDK (#1137)
Browse files Browse the repository at this point in the history
* feat: capture error details for the Kotlin SDK

Added `parseSDKError()` to extract Looker API error payloads from error responses

* PR feedback

- better regex
- code cleanup
  • Loading branch information
jkaster authored Aug 5, 2022
1 parent f4904e0 commit 9909206
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 69 deletions.
40 changes: 32 additions & 8 deletions kotlin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The Looker SDK for Kotlin provides a convenient way to communicate with the Looker API available on your Looker server.

**DISCLAIMER**: This is an _experimental_ version of the Looker SDK. Implementations are still subject to change, but SDK method calls are expected to work correctly. Please [report any issues](https://github.com/looker-open-source/sdk-codegen/issues) encountered, and indicate the SDK language in the report.
**NOTICE**: The Kotlin SDK is [community supported](https://docs.looker.com/reference/api-and-integration/api-sdk-support-policy). Please [report any issues](https://github.com/looker-open-source/sdk-codegen/issues) encountered, and indicate the SDK language in the report.

## Getting started

Expand Down Expand Up @@ -45,18 +45,16 @@ If this command fails the first time, read the [instructions for setting up `yar

### Use the SDK in your code

Looker 7.2 introduces an experimental API 4.0 that should be used for strongly-typed languages like Kotlin. (In fact, 4.0 was explicitly created to support languages like Swift and Kotlin.)
API 4.0 should be used for all strongly-typed languages like Kotlin. (API 4.0 was explicitly created to support languages like Swift and Kotlin.)

**NOTE**: For the Kotlin SDK, to correctly deserialize the JSON payloads from the Looker API, you **must** use the 4.0 client `LookerSDK`, not `Looker31SDK`.

When the SDK is installed and the server location and API credentials are configured in your `looker.ini` file, it's ready to be used.
When the SDK is installed and the server location and API credentials are configured in your `looker.ini` file, it's ready to be used. (There are [other ways](#environment-variable-configuration) of providing API credentials to the Kotlin SDK. Using an `.ini` file is a convenient option for development.)

Verify authentication works and that API calls will succeed with code similar to the following:

```kotlin
import com.looker.sdk.ApiSettings;
import com.looker.rtl.AuthSession;
import com.looker.sdk.LookerSDK;
import com.looker.sdk.ApiSettings
import com.looker.rtl.AuthSession
import com.looker.sdk.LookerSDK

val localIni = "./looker.ini"
val settings = ApiSettings.fromIniFile(localIni, "Looker")
Expand All @@ -69,6 +67,32 @@ val me = sdk.ok<User>(sdk.me())
val users = sdk.ok<Array<User>>(sdk.all_users())
```

### Capturing API Error responses

Detailed error responses from the Looker API can be captured using the `parseSDKError()` function. The following test shows how all error information from an API response can be captured:

```kotlin
@Test
fun testErrorReporting() {
try {
val props = ThemeSettings(
background_color = "invalid"
)
val theme = WriteTheme(
name = "'bogus!",
settings = props
)
val actual = sdk.ok<Theme>(sdk.validate_theme(theme))
assertNull(actual) // test should never get here
} catch (e: java.lang.Error) {
val error = parseSDKError(e.toString())
assertTrue(error.message.isNotEmpty())
assertTrue(error.errors.size == 2)
assertTrue(error.documentationUrl.isNotEmpty())
}
}
```

### More examples

Additional Kotlin SDK usage examples may be found in the [Kotlin Examples folder](/examples/kotlin)
Expand Down
6 changes: 0 additions & 6 deletions kotlin/src/main/com/looker/rtl/AuthToken.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,12 @@

package com.looker.rtl

import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.TypeAdapter
import com.google.gson.annotations.SerializedName
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import com.looker.sdk.AccessToken
import io.ktor.client.features.json.GsonSerializer
import io.ktor.client.features.json.defaultSerializer
import java.lang.reflect.Type
import java.time.LocalDateTime

data class AuthToken(
Expand Down
65 changes: 51 additions & 14 deletions kotlin/src/main/com/looker/rtl/Transport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,16 @@

package com.looker.rtl

import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import io.ktor.client.HttpClient
import io.ktor.client.call.receive
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.features.json.GsonSerializer
import io.ktor.client.features.json.defaultSerializer
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.request
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.HttpStatement
import io.ktor.http.takeFrom
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.features.json.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.runBlocking
import java.net.URLDecoder
import java.net.URLEncoder
Expand All @@ -53,6 +49,16 @@ import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
import kotlin.collections.Map
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.emptyMap
import kotlin.collections.filter
import kotlin.collections.forEach
import kotlin.collections.joinToString
import kotlin.collections.map
import kotlin.collections.set
import kotlin.collections.toMutableMap

sealed class SDKResponse {
/** A successful SDK call. */
Expand Down Expand Up @@ -351,3 +357,34 @@ class Transport(val options: TransportOptions) {
return builder
}
}

data class SDKErrorDetailInfo(
var message: String,
var field: String,
var code: String,
@SerializedName("documentation_url")
var documentationUrl: String,
)

data class SDKErrorInfo(
var message: String,
var errors: List<SDKErrorDetailInfo>,
@SerializedName("documentation_url")
var documentationUrl: String,
)

fun parseSDKError(msg: String) : SDKErrorInfo {
val rx = Regex("""\s+Text:\s+"(.*)"$""")
val info = rx.find(msg)
var result = SDKErrorInfo("", listOf(), "")
info?.let{
val (payload) = it.destructured
val gson = Gson()
result = gson.fromJson(payload, SDKErrorInfo::class.java)
// Ignore the linter suggestion to replace `.isNullOrEmpty()` with `.isEmpty()` because it's *wrong*
if (result.errors.isNullOrEmpty()) {
result.errors = listOf()
}
}
return result
}
55 changes: 22 additions & 33 deletions kotlin/src/test/TestMethods.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import com.looker.rtl.SDKResponse
import com.looker.rtl.parseSDKError
import com.looker.sdk.*
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import org.junit.Test
import kotlin.test.*

class TestMethods {
val sdk by lazy { TestConfig().sdk }
Expand Down Expand Up @@ -70,15 +68,6 @@ class TestMethods {
)
}

private fun slowQuery(): WriteQuery {
return WriteQuery(
"system__activity",
"dashboard",
arrayOf("dashboard.id", "dashboard.title", "dashboard.count"),
limit = "5000"
)
}

/*
Functions to prepare any data entities that might be missing for testing retrieval and iteration
*/
Expand Down Expand Up @@ -453,26 +442,6 @@ class TestMethods {
)
}

// TODO figure out a reliable way to queue up some running queries
// @Test fun testAllRunningQueries() {
// var running = false
// GlobalScope.launch {
// running = true
// val json = sdk.ok<String>(sdk.run_inline_query("json_detail", slowQuery()))
// print("slow query complete")
// running = false
// assertNotNull(json)
// }
// var tries = 0
// var list: Array<RunningQueries>
// do {
// list = sdk.ok(sdk.all_running_queries())
// Thread.sleep(100L) // block main thread to ensure query is running
// } while (running && list.count() == 0 && tries++ < 99)
// // assertEquals(running, false, "Running should have completed")
// assertNotEquals(list.count(), 0, "List should have at least one query")
// }

// @Test
fun testAllSchedulePlans() {
prepScheduledPlan()
Expand Down Expand Up @@ -566,4 +535,24 @@ class TestMethods {
{ id, _ -> sdk.workspace(id) }
)
}

@Test
fun testErrorReporting() {
try {
val props = ThemeSettings(
background_color = "invalid"
)
val theme = WriteTheme(
name = "'bogus!",
settings = props
)
val actual = sdk.ok<Theme>(sdk.validate_theme(theme))
assertNull(actual) // test should never get here
} catch (e: java.lang.Error) {
val error = parseSDKError(e.toString())
assertTrue(error.message.isNotEmpty())
assertTrue(error.errors.size == 2)
assertTrue(error.documentationUrl.isNotEmpty())
}
}
}
32 changes: 24 additions & 8 deletions kotlin/src/test/TestTransport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import com.looker.sdk.LOOKER_APPID
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.junit.Test
import java.io.File

class TestTransport {
val fullPath = "https://github.com/looker-open-source/sdk-codegen/"
Expand Down Expand Up @@ -109,12 +108,29 @@ class TestTransport {
""".trimIndent()
/// we use GSon so verify GSon handles ... flexible ... json
val gson = Gson()
var testModel = gson.fromJson(payload, TestModel::class.java)
assertEquals(testModel.string1, "1")
assertEquals(testModel.num1, 1)
assertEquals(testModel.string2, "2")
assertEquals(testModel.num2, 2)
assertEquals(testModel.string3, "3")
assertEquals(testModel.num3, 3)
val testModel = gson.fromJson(payload, TestModel::class.java)
assertEquals("1", testModel.string1)
assertEquals(1, testModel.num1)
assertEquals("2", testModel.string2)
assertEquals(2, testModel.num2)
assertEquals("3", testModel.string3)
assertEquals(3, testModel.num3)
}

@Test
fun testEmptyError() {
val error = parseSDKError("")
assertTrue(error.message.isEmpty())
assertTrue(error.errors.isEmpty())
assertTrue(error.documentationUrl.isEmpty())
}

@Test
fun testSummaryError() {
val payload = """Some kind of error happened! Text: "{"message":"Oops!","documentation_url":"MyBad"}""""
val error = parseSDKError(payload)
assertEquals("Oops!", error.message)
assertTrue(error.errors.isEmpty())
assertEquals("MyBad", error.documentationUrl)
}
}

0 comments on commit 9909206

Please sign in to comment.