Skip to content

Commit

Permalink
Use sealed interfaces to organize and enumerate all errors
Browse files Browse the repository at this point in the history
  • Loading branch information
jayohms committed Feb 27, 2024
1 parent 11bf873 commit a28816f
Show file tree
Hide file tree
Showing 18 changed files with 513 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import dev.hotwire.turbo.demo.util.SIGN_IN_URL
import dev.hotwire.turbo.fragments.TurboWebFragment
import dev.hotwire.turbo.nav.TurboNavGraphDestination
import dev.hotwire.turbo.views.TurboWebView
import dev.hotwire.turbo.errors.HttpError
import dev.hotwire.turbo.visit.TurboVisitAction.REPLACE
import dev.hotwire.turbo.visit.TurboVisitError
import dev.hotwire.turbo.errors.TurboVisitError
import dev.hotwire.turbo.visit.TurboVisitOptions

@TurboNavGraphDestination(uri = "turbo://fragment/web")
Expand Down Expand Up @@ -60,9 +61,10 @@ open class WebFragment : TurboWebFragment(), NavDestination {
}

override fun onVisitErrorReceived(location: String, error: TurboVisitError) {
when (error.code) {
401 -> navigate(SIGN_IN_URL, TurboVisitOptions(action = REPLACE))
else -> super.onVisitErrorReceived(location, error)
if (error is HttpError.ClientError.Unauthorized) {
navigate(SIGN_IN_URL, TurboVisitOptions(action = REPLACE))
} else {
super.onVisitErrorReceived(location, error)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import android.view.View
import android.view.ViewGroup
import dev.hotwire.turbo.demo.R
import dev.hotwire.turbo.nav.TurboNavGraphDestination
import dev.hotwire.turbo.visit.TurboVisitError
import dev.hotwire.turbo.errors.TurboVisitError

@TurboNavGraphDestination(uri = "turbo://fragment/web/home")
class WebHomeFragment : WebFragment() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import dev.hotwire.turbo.views.TurboView
import dev.hotwire.turbo.views.TurboWebView
import dev.hotwire.turbo.visit.TurboVisit
import dev.hotwire.turbo.visit.TurboVisitAction
import dev.hotwire.turbo.visit.TurboVisitError
import dev.hotwire.turbo.errors.TurboVisitError
import dev.hotwire.turbo.visit.TurboVisitOptions
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Expand Down
145 changes: 145 additions & 0 deletions turbo/src/main/kotlin/dev/hotwire/turbo/errors/HttpError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package dev.hotwire.turbo.errors

import android.webkit.WebResourceResponse

sealed interface HttpError : TurboVisitError {
val statusCode: Int
val reasonPhrase: String?

sealed interface ClientError : HttpError {
data object BadRequest : ClientError {
override val statusCode = 400
override val reasonPhrase = "Bad Request"
}

data object Unauthorized : ClientError {
override val statusCode = 401
override val reasonPhrase = "Unauthorized"
}

data object Forbidden : ClientError {
override val statusCode = 403
override val reasonPhrase = "Forbidden"
}

data object NotFound : ClientError {
override val statusCode = 404
override val reasonPhrase = "Not Found"
}

data object MethodNotAllowed : ClientError {
override val statusCode = 405
override val reasonPhrase = "Method Not Allowed"
}

data object NotAccessible : ClientError {
override val statusCode = 406
override val reasonPhrase = "Not Accessible"
}

data object ProxyAuthenticationRequired : ClientError {
override val statusCode = 407
override val reasonPhrase = "Proxy Authentication Required"
}

data object RequestTimeout : ClientError {
override val statusCode = 408
override val reasonPhrase = "Request Timeout"
}

data object Conflict : ClientError {
override val statusCode = 409
override val reasonPhrase = "Conflict"
}

data object MisdirectedRequest : ClientError {
override val statusCode = 421
override val reasonPhrase = "Misdirected Request"
}

data object UnprocessableEntity : ClientError {
override val statusCode = 422
override val reasonPhrase = "Unprocessable Entity"
}

data object PreconditionRequired : ClientError {
override val statusCode = 428
override val reasonPhrase = "Precondition Required"
}

data object TooManyRequests : ClientError {
override val statusCode = 429
override val reasonPhrase = "Too Many Requests"
}

data class Other(
override val statusCode: Int,
override val reasonPhrase: String?
) : ClientError
}

sealed interface ServerError : HttpError {
data object InternalServerError : ServerError {
override val statusCode = 500
override val reasonPhrase = "Internal Server Error"
}

data object NotImplemented : ServerError {
override val statusCode = 501
override val reasonPhrase = "Not Implemented"
}

data object BadGateway : ServerError {
override val statusCode = 502
override val reasonPhrase = "Bad Gateway"
}

data object ServiceUnavailable : ServerError {
override val statusCode = 503
override val reasonPhrase = "Service Unavailable"
}

data object GatewayTimeout : ServerError {
override val statusCode = 504
override val reasonPhrase = "Gateway Timeout"
}

data object HttpVersionNotSupported : ServerError {
override val statusCode = 505
override val reasonPhrase = "Http Version Not Supported"
}

data class Other(
override val statusCode: Int,
override val reasonPhrase: String?
) : ServerError
}

companion object {
fun from(errorResponse: WebResourceResponse): HttpError {
return getError(errorResponse.statusCode, errorResponse.reasonPhrase)
}

fun from(statusCode: Int): HttpError {
return getError(statusCode, null)
}

private fun getError(statusCode: Int, reasonPhrase: String?): HttpError {
if (statusCode in 400..499) {
return ClientError::class.sealedSubclasses
.mapNotNull { it.objectInstance }
.firstOrNull { it.statusCode == statusCode }
?: ClientError.Other(statusCode, reasonPhrase)
}

if (statusCode in 500..599) {
return ServerError::class.sealedSubclasses
.map { it.objectInstance }
.firstOrNull { it?.statusCode == statusCode }
?: ServerError.Other(statusCode, reasonPhrase)
}

throw IllegalArgumentException("Invalid HTTP error status code: $statusCode")
}
}
}
6 changes: 6 additions & 0 deletions turbo/src/main/kotlin/dev/hotwire/turbo/errors/LoadError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.hotwire.turbo.errors

sealed interface LoadError : TurboVisitError {
data object NotPresent : LoadError
data object NotReady : LoadError
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dev.hotwire.turbo.errors

sealed interface TurboVisitError
125 changes: 125 additions & 0 deletions turbo/src/main/kotlin/dev/hotwire/turbo/errors/WebError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package dev.hotwire.turbo.errors

import androidx.webkit.WebResourceErrorCompat
import androidx.webkit.WebViewClientCompat
import androidx.webkit.WebViewFeature
import androidx.webkit.WebViewFeature.isFeatureSupported

sealed interface WebError : TurboVisitError {
val errorCode: Int
val description: String?

data object Unknown : WebError {
override val errorCode = WebViewClientCompat.ERROR_UNKNOWN
override val description = "Unknown"
}

data object HostLookup : WebError {
override val errorCode = WebViewClientCompat.ERROR_HOST_LOOKUP
override val description = "Host Lookup"
}

data object UnsupportedAuthScheme : WebError {
override val errorCode = WebViewClientCompat.ERROR_UNSUPPORTED_AUTH_SCHEME
override val description = "Unsupported Auth Scheme"
}

data object Authentication : WebError {
override val errorCode = WebViewClientCompat.ERROR_AUTHENTICATION
override val description = "Authentication"
}

data object ProxyAuthentication : WebError {
override val errorCode = WebViewClientCompat.ERROR_PROXY_AUTHENTICATION
override val description = "Proxy Authentication"
}

data object Connect : WebError {
override val errorCode = WebViewClientCompat.ERROR_CONNECT
override val description = "Connect"
}

data object IO : WebError {
override val errorCode = WebViewClientCompat.ERROR_IO
override val description = "IO"
}

data object Timeout : WebError {
override val errorCode = WebViewClientCompat.ERROR_TIMEOUT
override val description = "Timeout"
}

data object RedirectLoop : WebError {
override val errorCode = WebViewClientCompat.ERROR_REDIRECT_LOOP
override val description = "Redirect Loop"
}

data object UnsupportedScheme : WebError {
override val errorCode = WebViewClientCompat.ERROR_UNSUPPORTED_SCHEME
override val description = "Unsupported Scheme"
}

data object FailedSslHandshake : WebError {
override val errorCode = WebViewClientCompat.ERROR_FAILED_SSL_HANDSHAKE
override val description = "Failed SSL Handshake"
}

data object BadUrl : WebError {
override val errorCode = WebViewClientCompat.ERROR_BAD_URL
override val description = "Bad URL"
}

data object File : WebError {
override val errorCode = WebViewClientCompat.ERROR_FILE
override val description = "File"
}

data object FileNotFound : WebError {
override val errorCode = WebViewClientCompat.ERROR_FILE_NOT_FOUND
override val description = "File Not Found"
}

data object TooManyRequests : WebError {
override val errorCode = WebViewClientCompat.ERROR_TOO_MANY_REQUESTS
override val description = "Too Many Requests"
}

data object UnsafeResource : WebError {
override val errorCode = WebViewClientCompat.ERROR_UNSAFE_RESOURCE
override val description = "Unsafe Resource"
}

data class Other(
override val errorCode: Int,
override val description: String?
) : WebError

companion object {
fun from(error: WebResourceErrorCompat): WebError {
val errorCode = if (isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) {
error.errorCode
} else {
0
}

val description = if (isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION)) {
error.description.toString()
} else {
null
}

return getError(errorCode, description)
}

fun from(errorCode: Int): WebError {
return getError(errorCode, null)
}

private fun getError(errorCode: Int, description: String?): WebError {
return WebError::class.sealedSubclasses
.mapNotNull { it.objectInstance }
.firstOrNull { it.errorCode == errorCode }
?: Other(errorCode, description)
}
}
}
59 changes: 59 additions & 0 deletions turbo/src/main/kotlin/dev/hotwire/turbo/errors/WebSslError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package dev.hotwire.turbo.errors

import android.net.http.SslError

sealed interface WebSslError : TurboVisitError {
val errorCode: Int
val description: String?

data object NotYetValid : WebSslError {
override val errorCode = SslError.SSL_NOTYETVALID
override val description = "Not Yet Valid"
}

data object Expired : WebSslError {
override val errorCode = SslError.SSL_EXPIRED
override val description = "Expired"
}

data object IdMismatch : WebSslError {
override val errorCode = SslError.SSL_IDMISMATCH
override val description = "ID Mismatch"
}

data object Untrusted : WebSslError {
override val errorCode = SslError.SSL_UNTRUSTED
override val description = "Untrusted"
}

data object DateInvalid : WebSslError {
override val errorCode = SslError.SSL_DATE_INVALID
override val description = "Date Invalid"
}

data object Invalid : WebSslError {
override val errorCode = SslError.SSL_INVALID
override val description = "Invalid"
}

data class Other(override val errorCode: Int) : WebSslError {
override val description = null
}

companion object {
fun from(error: SslError): WebSslError {
return getError(error.primaryError)
}

fun from(errorCode: Int): WebSslError {
return getError(errorCode)
}

private fun getError(errorCode: Int): WebSslError {
return WebSslError::class.sealedSubclasses
.mapNotNull { it.objectInstance }
.firstOrNull { it.errorCode == errorCode }
?: Other(errorCode)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import dev.hotwire.turbo.delegates.TurboWebFragmentDelegate
import dev.hotwire.turbo.util.TURBO_REQUEST_CODE_FILES
import dev.hotwire.turbo.views.TurboView
import dev.hotwire.turbo.views.TurboWebChromeClient
import dev.hotwire.turbo.visit.TurboVisitError
import dev.hotwire.turbo.errors.TurboVisitError

/**
* The base class from which all bottom sheet web fragments in a
Expand Down
Loading

0 comments on commit a28816f

Please sign in to comment.