android: simplify local API client
Updates tailscale/corp#18202 Signed-off-by: Percy Wegmann <percy@tailscale.com>pull/213/head
parent
e16303e1d8
commit
d42329e2e2
@ -0,0 +1,230 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.localapi
|
||||
|
||||
import android.util.Log
|
||||
import com.tailscale.ipn.ui.model.BugReportID
|
||||
import com.tailscale.ipn.ui.model.Errors
|
||||
import com.tailscale.ipn.ui.model.Ipn
|
||||
import com.tailscale.ipn.ui.model.IpnLocal
|
||||
import com.tailscale.ipn.ui.model.IpnState
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
private object Endpoint {
|
||||
const val DEBUG = "debug"
|
||||
const val DEBUG_LOG = "debug-log"
|
||||
const val BUG_REPORT = "bugreport"
|
||||
const val PREFS = "prefs"
|
||||
const val FILE_TARGETS = "file-targets"
|
||||
const val UPLOAD_METRICS = "upload-client-metrics"
|
||||
const val START = "start"
|
||||
const val LOGIN_INTERACTIVE = "login-interactive"
|
||||
const val RESET_AUTH = "reset-auth"
|
||||
const val LOGOUT = "logout"
|
||||
const val PROFILES = "profiles/"
|
||||
const val PROFILES_CURRENT = "profiles/current"
|
||||
const val STATUS = "status"
|
||||
const val TKA_STATUS = "tka/status"
|
||||
const val TKA_SIGN = "tka/sign"
|
||||
const val TKA_VERIFY_DEEP_LINK = "tka/verify-deeplink"
|
||||
const val PING = "ping"
|
||||
const val FILES = "files"
|
||||
const val FILE_PUT = "file-put"
|
||||
const val TAILFS_SERVER_ADDRESS = "tailfs/fileserver-address"
|
||||
}
|
||||
|
||||
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
|
||||
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
|
||||
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
|
||||
|
||||
/**
|
||||
* Client provides a mechanism for calling Go's LocalAPIClient. Every LocalAPI endpoint has a
|
||||
* corresponding method on this Client.
|
||||
*/
|
||||
class Client(private val scope: CoroutineScope) {
|
||||
fun status(responseHandler: StatusResponseHandler) {
|
||||
get(Endpoint.STATUS, responseHandler = responseHandler)
|
||||
}
|
||||
|
||||
fun bugReportId(responseHandler: BugReportIdHandler) {
|
||||
post(Endpoint.BUG_REPORT, responseHandler = responseHandler)
|
||||
}
|
||||
|
||||
fun prefs(responseHandler: PrefsHandler) {
|
||||
get(Endpoint.PREFS, responseHandler = responseHandler)
|
||||
}
|
||||
|
||||
fun editPrefs(
|
||||
prefs: Ipn.MaskedPrefs, responseHandler: (Result<Ipn.Prefs>) -> Unit
|
||||
) {
|
||||
val body = Json.encodeToString(prefs).toByteArray()
|
||||
return patch(Endpoint.PREFS, body, responseHandler = responseHandler)
|
||||
}
|
||||
|
||||
fun profiles(responseHandler: (Result<List<IpnLocal.LoginProfile>>) -> Unit) {
|
||||
get(Endpoint.PROFILES, responseHandler = responseHandler)
|
||||
}
|
||||
|
||||
fun currentProfile(responseHandler: (Result<IpnLocal.LoginProfile>) -> Unit) {
|
||||
return get(Endpoint.PROFILES_CURRENT, responseHandler = responseHandler)
|
||||
}
|
||||
|
||||
fun startLoginInteractive(responseHandler: (Result<String>) -> Unit) {
|
||||
return post(Endpoint.LOGIN_INTERACTIVE, responseHandler = responseHandler)
|
||||
}
|
||||
|
||||
fun logout(responseHandler: (Result<String>) -> Unit) {
|
||||
return post(Endpoint.LOGOUT, responseHandler = responseHandler)
|
||||
}
|
||||
|
||||
private inline fun <reified T> get(
|
||||
path: String, body: ByteArray? = null, noinline responseHandler: (Result<T>) -> Unit
|
||||
) {
|
||||
Request(
|
||||
scope = scope,
|
||||
method = "GET",
|
||||
path = path,
|
||||
body = body,
|
||||
responseType = typeOf<T>(),
|
||||
responseHandler = responseHandler
|
||||
).execute()
|
||||
}
|
||||
|
||||
private inline fun <reified T> put(
|
||||
path: String, body: ByteArray? = null, noinline responseHandler: (Result<T>) -> Unit
|
||||
) {
|
||||
Request(
|
||||
scope = scope,
|
||||
method = "PUT",
|
||||
path = path,
|
||||
body = body,
|
||||
responseType = typeOf<T>(),
|
||||
responseHandler = responseHandler
|
||||
).execute()
|
||||
}
|
||||
|
||||
private inline fun <reified T> post(
|
||||
path: String, body: ByteArray? = null, noinline responseHandler: (Result<T>) -> Unit
|
||||
) {
|
||||
Request(
|
||||
scope = scope,
|
||||
method = "POST",
|
||||
path = path,
|
||||
body = body,
|
||||
responseType = typeOf<T>(),
|
||||
responseHandler = responseHandler
|
||||
).execute()
|
||||
}
|
||||
|
||||
private inline fun <reified T> patch(
|
||||
path: String, body: ByteArray? = null, noinline responseHandler: (Result<T>) -> Unit
|
||||
) {
|
||||
Request(
|
||||
scope = scope,
|
||||
method = "PATCH",
|
||||
path = path,
|
||||
body = body,
|
||||
responseType = typeOf<T>(),
|
||||
responseHandler = responseHandler
|
||||
).execute()
|
||||
}
|
||||
|
||||
private inline fun <reified T> delete(
|
||||
path: String, noinline responseHandler: (Result<T>) -> Unit
|
||||
) {
|
||||
Request(
|
||||
scope = scope,
|
||||
method = "DELETE",
|
||||
path = path,
|
||||
responseType = typeOf<T>(),
|
||||
responseHandler = responseHandler
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
class Request<T>(
|
||||
private val scope: CoroutineScope,
|
||||
private val method: String,
|
||||
path: String,
|
||||
private val body: ByteArray? = null,
|
||||
private val responseType: KType,
|
||||
private val responseHandler: (Result<T>) -> Unit
|
||||
) {
|
||||
private val fullPath = "/localapi/v0/$path"
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LocalAPIRequest"
|
||||
|
||||
private val jsonDecoder = Json { ignoreUnknownKeys = true }
|
||||
private val isReady = CompletableDeferred<Boolean>()
|
||||
|
||||
// Called by the backend when the localAPI is ready to accept requests.
|
||||
@JvmStatic
|
||||
@Suppress("unused")
|
||||
fun onReady() {
|
||||
isReady.complete(true)
|
||||
Log.d(TAG, "Ready")
|
||||
}
|
||||
}
|
||||
|
||||
// Perform a request to the local API in the go backend. This is
|
||||
// the primary JNI method for servicing a localAPI call. This
|
||||
// is GUARANTEED to call back into onResponse.
|
||||
// @see cmd/localapiclient/localapishim.go
|
||||
//
|
||||
// method: The HTTP method to use.
|
||||
// request: The path to the localAPI endpoint.
|
||||
// body: The body of the request.
|
||||
private external fun doRequest(method: String, request: String, body: ByteArray?)
|
||||
|
||||
fun execute() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
isReady.await()
|
||||
Log.d(TAG, "Executing request:${method}:${fullPath}")
|
||||
doRequest(method, fullPath, body)
|
||||
}
|
||||
}
|
||||
|
||||
// This is called from the JNI layer to publish responses.
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Suppress("unused", "UNCHECKED_CAST")
|
||||
fun onResponse(respData: ByteArray) {
|
||||
Log.d(TAG, "Response for request: $fullPath")
|
||||
|
||||
val response: Result<T> = when (responseType) {
|
||||
typeOf<String>() -> Result.success(respData.decodeToString() as T)
|
||||
else -> try {
|
||||
Result.success(
|
||||
jsonDecoder.decodeFromStream(
|
||||
Json.serializersModule.serializer(responseType), respData.inputStream()
|
||||
) as T
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
// If we couldn't parse the response body, assume it's an error response
|
||||
try {
|
||||
val error =
|
||||
jsonDecoder.decodeFromStream<Errors.GenericError>(respData.inputStream())
|
||||
throw Exception(error.error)
|
||||
} catch (t: Throwable) {
|
||||
Result.failure(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The response handler will invoked internally by the request parser
|
||||
scope.launch {
|
||||
responseHandler(response)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.localapi
|
||||
|
||||
import android.util.Log
|
||||
import com.tailscale.ipn.ui.model.BugReportID
|
||||
import com.tailscale.ipn.ui.model.Ipn
|
||||
import com.tailscale.ipn.ui.model.IpnLocal
|
||||
import com.tailscale.ipn.ui.model.IpnState
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
|
||||
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
|
||||
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
|
||||
|
||||
class LocalApiClient(private val scope: CoroutineScope) {
|
||||
init {
|
||||
Log.d("LocalApiClient", "LocalApiClient created")
|
||||
}
|
||||
|
||||
companion object {
|
||||
val isReady = CompletableDeferred<Boolean>()
|
||||
|
||||
// Called by the backend when the localAPI is ready to accept requests.
|
||||
@JvmStatic
|
||||
@Suppress("unused")
|
||||
fun onReady() {
|
||||
isReady.complete(true)
|
||||
Log.d("LocalApiClient", "LocalApiClient is ready")
|
||||
}
|
||||
}
|
||||
|
||||
// Perform a request to the local API in the go backend. This is
|
||||
// the primary JNI method for servicing a localAPI call. This
|
||||
// is GUARANTEED to call back into onResponse with the response
|
||||
// from the backend with a matching cookie.
|
||||
// @see cmd/localapiclient/localapishim.go
|
||||
//
|
||||
// request: The path to the localAPI endpoint.
|
||||
// method: The HTTP method to use.
|
||||
// body: The body of the request.
|
||||
// cookie: A unique identifier for this request. This is used map responses to
|
||||
// the corresponding request. Cookies must be unique for each request.
|
||||
private external fun doRequest(
|
||||
request: String, method: String, body: ByteArray?, cookie: String
|
||||
)
|
||||
|
||||
private fun <T> executeRequest(request: LocalAPIRequest<T>) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
isReady.await()
|
||||
Log.d("LocalApiClient", "Executing request:${request.method}:${request.path}")
|
||||
requests[request.cookie] = request
|
||||
doRequest(request.path, request.method, request.body, request.cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// This is called from the JNI layer to publish localAPIResponses. This should execute on the
|
||||
// same thread that called doRequest.
|
||||
@Suppress("unused")
|
||||
fun onResponse(response: ByteArray, cookie: String) {
|
||||
requests.remove(cookie)?.let { request ->
|
||||
Log.d("LocalApiClient", "Response for request:${request.path} cookie:${request.cookie}")
|
||||
// The response handler will invoked internally by the request parser
|
||||
scope.launch {
|
||||
request.parser(response)
|
||||
}
|
||||
} ?: { Log.e("LocalApiClient", "Received response for unknown request: $cookie") }
|
||||
}
|
||||
|
||||
// Tracks in-flight requests and their callback handlers by cookie. This should
|
||||
// always be manipulated via the addRequest and removeRequest methods.
|
||||
private var requests = ConcurrentHashMap<String, LocalAPIRequest<*>>()
|
||||
|
||||
// localapi Invocations
|
||||
|
||||
fun getStatus(responseHandler: StatusResponseHandler) {
|
||||
val req = LocalAPIRequest.status(responseHandler)
|
||||
executeRequest(req)
|
||||
}
|
||||
|
||||
fun getBugReportId(responseHandler: BugReportIdHandler) {
|
||||
val req = LocalAPIRequest.bugReportId(responseHandler)
|
||||
executeRequest(req)
|
||||
}
|
||||
|
||||
fun getPrefs(responseHandler: PrefsHandler) {
|
||||
val req = LocalAPIRequest.prefs(responseHandler)
|
||||
executeRequest(req)
|
||||
}
|
||||
|
||||
fun editPrefs(prefs: Ipn.MaskedPrefs, responseHandler: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val req = LocalAPIRequest.editPrefs(prefs, responseHandler)
|
||||
executeRequest(req)
|
||||
}
|
||||
|
||||
fun getProfiles(responseHandler: (Result<List<IpnLocal.LoginProfile>>) -> Unit) {
|
||||
val req = LocalAPIRequest.profiles(responseHandler)
|
||||
executeRequest(req)
|
||||
}
|
||||
|
||||
fun getCurrentProfile(responseHandler: (Result<IpnLocal.LoginProfile>) -> Unit) {
|
||||
val req = LocalAPIRequest.currentProfile(responseHandler)
|
||||
executeRequest(req)
|
||||
}
|
||||
|
||||
fun startLoginInteractive() {
|
||||
val req = LocalAPIRequest.startLoginInteractive { result ->
|
||||
result.success?.let { Log.d("LocalApiClient", "Login started: $it") }
|
||||
?: run { Log.e("LocalApiClient", "Error starting login: ${result.error}") }
|
||||
}
|
||||
executeRequest<String>(req)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
val req = LocalAPIRequest.logout { result ->
|
||||
result.success?.let { Log.d("LocalApiClient", "Logout started: $it") }
|
||||
?: run { Log.e("LocalApiClient", "Error starting logout: ${result.error}") }
|
||||
}
|
||||
executeRequest<String>(req)
|
||||
}
|
||||
|
||||
// (jonathan) TODO: A (likely) exhaustive list of localapi endpoints required for
|
||||
// a fully functioning client. This is a work in progress and will be updated
|
||||
// See: corp/xcode/Shared/LocalAPIClient.swift for the various verbs, parameters,
|
||||
// and body contents for each endpoint. Endpoints are defined in LocalAPIEndpoint
|
||||
//
|
||||
// fetchFileTargets
|
||||
// sendFiles
|
||||
// getWaitingFiles
|
||||
// recieveWaitingFile
|
||||
// inidicateFileRecieved
|
||||
// debug
|
||||
// debugLog
|
||||
// uploadClientMetrics
|
||||
// start
|
||||
// startLoginInteractive
|
||||
// logout
|
||||
// addProfile
|
||||
// switchProfile
|
||||
// deleteProfile
|
||||
// tailnetLocalStatus
|
||||
// signNode
|
||||
// verifyDeepling
|
||||
// ping
|
||||
// setTailFSFileServerAddress
|
||||
init {
|
||||
Log.d("LocalApiClient", "LocalApiClient created")
|
||||
}
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.localapi
|
||||
|
||||
import com.tailscale.ipn.ui.model.BugReportID
|
||||
import com.tailscale.ipn.ui.model.Errors
|
||||
import com.tailscale.ipn.ui.model.Ipn
|
||||
import com.tailscale.ipn.ui.model.IpnLocal
|
||||
import com.tailscale.ipn.ui.model.IpnState
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import java.util.UUID
|
||||
|
||||
private object Endpoint {
|
||||
const val DEBUG = "debug"
|
||||
const val DEBUG_LOG = "debug-log"
|
||||
const val BUG_REPORT = "bugreport"
|
||||
const val PREFS = "prefs"
|
||||
const val FILE_TARGETS = "file-targets"
|
||||
const val UPLOAD_METRICS = "upload-client-metrics"
|
||||
const val START = "start"
|
||||
const val LOGIN_INTERACTIVE = "login-interactive"
|
||||
const val RESET_AUTH = "reset-auth"
|
||||
const val LOGOUT = "logout"
|
||||
const val PROFILES = "profiles/"
|
||||
const val PROFILES_CURRENT = "profiles/current"
|
||||
const val STATUS = "status"
|
||||
const val TKA_STATUS = "tka/status"
|
||||
const val TKA_SIGN = "tka/sign"
|
||||
const val TKA_VERIFY_DEEP_LINK = "tka/verify-deeplink"
|
||||
const val PING = "ping"
|
||||
const val FILES = "files"
|
||||
const val FILE_PUT = "file-put"
|
||||
const val TAILFS_SERVER_ADDRESS = "tailfs/fileserver-address"
|
||||
}
|
||||
|
||||
// Potential local and upstream errors. Error handling in localapi in the go layer
|
||||
// is inconsistent but different clients already deal with that inconsistency so
|
||||
// 'fixing' it will likely break other things.
|
||||
//
|
||||
// For now, anything that isn't an { error: "message" } will be passed along
|
||||
// as UNPARSEABLE_RESPONSE. We can add additional error translation in the parseError
|
||||
// method as needed.
|
||||
//
|
||||
// (jonathan) TODO: Audit local API for all of the possible error results and clean
|
||||
// it up if possible.
|
||||
|
||||
enum class APIErrorVals(val rawValue: String) {
|
||||
UNPARSEABLE_RESPONSE("Unparseable localAPI response"), NOT_READY("Not Ready"), NO_PREFS("Current prefs not available");
|
||||
|
||||
fun toError(): Error {
|
||||
return Error(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
class LocalAPIRequest<T>(
|
||||
path: String,
|
||||
val method: String,
|
||||
val body: ByteArray? = null,
|
||||
val parser: (ByteArray) -> Unit,
|
||||
) {
|
||||
val path = "/localapi/v0/$path"
|
||||
val cookie = UUID.randomUUID().toString()
|
||||
|
||||
companion object {
|
||||
|
||||
val decoder = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun <T> get(path: String, body: ByteArray? = null, parser: (ByteArray) -> Unit) =
|
||||
LocalAPIRequest<T>(
|
||||
method = "GET", path = path, body = body, parser = parser
|
||||
)
|
||||
|
||||
fun <T> put(path: String, body: ByteArray? = null, parser: (ByteArray) -> Unit) =
|
||||
LocalAPIRequest<T>(
|
||||
method = "PUT", path = path, body = body, parser = parser
|
||||
)
|
||||
|
||||
fun <T> post(path: String, body: ByteArray? = null, parser: (ByteArray) -> Unit) =
|
||||
LocalAPIRequest<T>(
|
||||
method = "POST", path = path, body = body, parser = parser
|
||||
)
|
||||
|
||||
fun <T> patch(path: String, body: ByteArray? = null, parser: (ByteArray) -> Unit) =
|
||||
LocalAPIRequest<T>(
|
||||
method = "PATCH", path = path, body = body, parser = parser
|
||||
)
|
||||
|
||||
fun status(responseHandler: StatusResponseHandler): LocalAPIRequest<IpnState.Status> {
|
||||
return get(Endpoint.STATUS) { resp ->
|
||||
responseHandler(decode<IpnState.Status>(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun bugReportId(responseHandler: BugReportIdHandler): LocalAPIRequest<BugReportID> {
|
||||
return post(Endpoint.BUG_REPORT) { resp ->
|
||||
responseHandler(parseString(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun prefs(responseHandler: PrefsHandler): LocalAPIRequest<Ipn.Prefs> {
|
||||
return get(Endpoint.PREFS) { resp ->
|
||||
responseHandler(decode<Ipn.Prefs>(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun editPrefs(
|
||||
prefs: Ipn.MaskedPrefs, responseHandler: (Result<Ipn.Prefs>) -> Unit
|
||||
): LocalAPIRequest<Ipn.Prefs> {
|
||||
val body = Json.encodeToString(prefs).toByteArray()
|
||||
return patch(Endpoint.PREFS, body) { resp ->
|
||||
responseHandler(decode<Ipn.Prefs>(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun profiles(responseHandler: (Result<List<IpnLocal.LoginProfile>>) -> Unit): LocalAPIRequest<List<IpnLocal.LoginProfile>> {
|
||||
return get(Endpoint.PROFILES) { resp ->
|
||||
responseHandler(decode<List<IpnLocal.LoginProfile>>(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun currentProfile(responseHandler: (Result<IpnLocal.LoginProfile>) -> Unit): LocalAPIRequest<IpnLocal.LoginProfile> {
|
||||
return get(Endpoint.PROFILES_CURRENT) { resp ->
|
||||
responseHandler(decode<IpnLocal.LoginProfile>(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun startLoginInteractive(responseHandler: (Result<String>) -> Unit): LocalAPIRequest<String> {
|
||||
return post(Endpoint.LOGIN_INTERACTIVE) { resp ->
|
||||
responseHandler(parseString(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun logout(responseHandler: (Result<String>) -> Unit): LocalAPIRequest<String> {
|
||||
return post(Endpoint.LOGOUT) { resp ->
|
||||
responseHandler(parseString(resp))
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the response was a generic error
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun parseError(respData: ByteArray): Error {
|
||||
return try {
|
||||
val err = Json.decodeFromStream<Errors.GenericError>(respData.inputStream())
|
||||
Error(err.error)
|
||||
} catch (e: Exception) {
|
||||
Error(APIErrorVals.UNPARSEABLE_RESPONSE.toError())
|
||||
}
|
||||
}
|
||||
|
||||
// Handles responses that are raw strings. Returns an error result if the string
|
||||
// is empty
|
||||
private fun parseString(respData: ByteArray): Result<String> {
|
||||
return if (respData.isNotEmpty()) Result(respData.decodeToString())
|
||||
else Result(APIErrorVals.UNPARSEABLE_RESPONSE.toError())
|
||||
}
|
||||
|
||||
// Attempt to decode the response into the expected type. If that fails, then try
|
||||
// parsing as an error.
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private inline fun <reified T> decode(respData: ByteArray): Result<T> {
|
||||
return try {
|
||||
val message = decoder.decodeFromStream<T>(respData.inputStream())
|
||||
Result(message)
|
||||
} catch (e: Exception) {
|
||||
Result(parseError(respData))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.localapi
|
||||
|
||||
// Go-like result type with an optional value and an optional Error
|
||||
// This guarantees that only one of the two is non-null
|
||||
class Result<T> {
|
||||
val success: T?
|
||||
val error: Error?
|
||||
|
||||
private constructor(success: T?, error: Error?) {
|
||||
if (success != null && error != null) {
|
||||
throw IllegalArgumentException("Result cannot have both a success and an error")
|
||||
}
|
||||
if (success == null && error == null) {
|
||||
throw IllegalArgumentException("Result must have either a success or an error")
|
||||
}
|
||||
|
||||
this.success = success
|
||||
this.error = error
|
||||
}
|
||||
|
||||
constructor(success: T) : this(success, null)
|
||||
constructor(error: Error) : this(null, error)
|
||||
|
||||
var successful: Boolean = false
|
||||
get() = success != null
|
||||
|
||||
var failed: Boolean = false
|
||||
get() = error != null
|
||||
}
|
Loading…
Reference in New Issue