Skip to content
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

feature: GraphQL support (issue #326) #327

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class NetworkData {
)

data class Response(
val request: Request,
val statusCode: Int,
val body: Body?,
val headers: Map<String, String?>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.pluto.plugins.network.intercept
import com.pluto.plugins.network.internal.Status
import com.pluto.plugins.network.internal.interceptor.logic.mapCode2Message
import io.ktor.http.ContentType
import org.json.JSONObject

class NetworkData {

Expand All @@ -11,42 +12,80 @@ class NetworkData {
val method: String,
val body: Body?,
val headers: Map<String, String?>,
val sentTimestamp: Long
val sentTimestamp: Long,
) {
data class GraphqlData(
val queryType: String,
val queryName: String,
)

val graphqlData: GraphqlData? = parseGraphqlData()

private fun parseGraphqlData(): GraphqlData? {
if (method != "POST" ||
body == null ||
!body.isLikelyJson
pavelperc marked this conversation as resolved.
Show resolved Hide resolved
) return null
val json = runCatching { JSONObject(body!!.body.toString()) }.getOrNull() ?: return null
val query = json.optString("query") ?: return null
val match = graqphlQueryRegex.find(query)?.groupValues ?: return null
return GraphqlData(
queryType = match[1],
queryName = match[2],
)
}

internal val isGzipped: Boolean
get() = headers["Content-Encoding"].equals("gzip", ignoreCase = true)
}

data class Response(
val request: Request,
private val statusCode: Int,
val body: Body?,
val headers: Map<String, String?>,
val sentTimestamp: Long,
val receiveTimestamp: Long,
val protocol: String = "",
val fromDiskCache: Boolean = false
val fromDiskCache: Boolean = false,
) {
val hasGraphqlErrors = parseHasGraphqlErrors()

internal val status: Status
get() = Status(statusCode, mapCode2Message(statusCode))
get() = Status(statusCode, getStatusMessage())
val isSuccessful: Boolean
get() = statusCode in 200..299
get() = statusCode in 200..299 && !hasGraphqlErrors
internal val isGzipped: Boolean
get() = headers["Content-Encoding"].equals("gzip", ignoreCase = true)

private fun getStatusMessage() = mapCode2Message(statusCode) +
if (hasGraphqlErrors) ", Response with errors" else ""

private fun parseHasGraphqlErrors(): Boolean {
if (request.graphqlData == null ||
body == null ||
!body.isLikelyJson
) return false
val json = runCatching { JSONObject(body!!.body.toString()) }.getOrNull() ?: return false
return json.has("errors")
}
}

data class Body(
val body: CharSequence,
val contentType: String
val contentType: String,
) {
private val contentTypeInternal: ContentType = ContentType.parse(contentType)
private val mediaType: String = contentTypeInternal.contentType
internal val mediaSubtype: String = contentTypeInternal.contentSubtype
internal val isBinary: Boolean = BINARY_MEDIA_TYPES.contains(mediaType)
val sizeInBytes: Long = body.length.toLong()
internal val mediaTypeFull: String = "$mediaType/$mediaSubtype"
val isLikelyJson get() = !isBinary && body.startsWith('{')
}

companion object {
internal val BINARY_MEDIA_TYPES = listOf("audio", "video", "image", "font")
private val graqphlQueryRegex = Regex("""\b(query|mutation)\s+(\w+)""")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.pluto.plugins.network.R
import com.pluto.plugins.network.databinding.PlutoNetworkItemNetworkBinding
import com.pluto.plugins.network.intercept.NetworkData.Response
Expand All @@ -30,16 +31,21 @@ internal class ApiItemHolder(parent: ViewGroup, actionListener: DiffAwareAdapter
private val error = binding.error
private val timeElapsed = binding.timeElapsed
private val proxyIndicator = binding.proxyIndicator
private val graphqlIcon = binding.graphqlIcon

override fun onBind(item: ListItem) {
if (item is ApiCallData) {
host.text = Url(item.request.url).host
timeElapsed.text = item.request.sentTimestamp.asTimeElapsed()
binding.root.setBackgroundColor(context.color(R.color.pluto___transparent))

val method = (item.request.graphqlData?.queryType ?: item.request.method).uppercase()
val urlOrQuery = item.request.graphqlData?.queryName ?: Url(item.request.url).encodedPath
graphqlIcon.isVisible = item.request.graphqlData != null

url.setSpan {
append(fontColor(item.request.method.uppercase(), context.color(R.color.pluto___text_dark_60)))
append(" ${Url(item.request.url).encodedPath}")
append(fontColor(method, context.color(R.color.pluto___text_dark_60)))
append(" $urlOrQuery")
}
progress.visibility = VISIBLE
status.visibility = INVISIBLE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="400"
android:viewportHeight="400">
<path
android:pathData="M57.47,302.66l-14.38,-8.3l160.15,-277.38l14.38,8.3z"
android:fillColor="#E535AB"/>
<path
android:pathData="M39.8,272.2h320.3v16.6h-320.3z"
android:fillColor="#E535AB"/>
<path
android:pathData="M206.35,374.03l-160.21,-92.5l8.3,-14.38l160.21,92.5z"
android:fillColor="#E535AB"/>
<path
android:pathData="M345.52,132.95l-160.21,-92.5l8.3,-14.38l160.21,92.5z"
android:fillColor="#E535AB"/>
<path
android:pathData="M54.48,132.88l-8.3,-14.38l160.21,-92.5l8.3,14.38z"
android:fillColor="#E535AB"/>
<path
android:pathData="M342.57,302.66l-160.15,-277.38l14.38,-8.3l160.15,277.38z"
android:fillColor="#E535AB"/>
<path
android:pathData="M52.5,107.5h16.6v185h-16.6z"
android:fillColor="#E535AB"/>
<path
android:pathData="M330.9,107.5h16.6v185h-16.6z"
android:fillColor="#E535AB"/>
<path
android:pathData="M203.52,367l-7.25,-12.56l139.34,-80.45l7.25,12.56z"
android:fillColor="#E535AB"/>
<path
android:pathData="M369.5,297.9c-9.6,16.7 -31,22.4 -47.7,12.8c-16.7,-9.6 -22.4,-31 -12.8,-47.7c9.6,-16.7 31,-22.4 47.7,-12.8C373.5,259.9 379.2,281.2 369.5,297.9"
android:fillColor="#E535AB"/>
<path
android:pathData="M90.9,137c-9.6,16.7 -31,22.4 -47.7,12.8c-16.7,-9.6 -22.4,-31 -12.8,-47.7c9.6,-16.7 31,-22.4 47.7,-12.8C94.8,99 100.5,120.3 90.9,137"
android:fillColor="#E535AB"/>
<path
android:pathData="M30.5,297.9c-9.6,-16.7 -3.9,-38 12.8,-47.7c16.7,-9.6 38,-3.9 47.7,12.8c9.6,16.7 3.9,38 -12.8,47.7C61.4,320.3 40.1,314.6 30.5,297.9"
android:fillColor="#E535AB"/>
<path
android:pathData="M309.1,137c-9.6,-16.7 -3.9,-38 12.8,-47.7c16.7,-9.6 38,-3.9 47.7,12.8c9.6,16.7 3.9,38 -12.8,47.7C340.1,159.4 318.7,153.7 309.1,137"
android:fillColor="#E535AB"/>
<path
android:pathData="M200,395.8c-19.3,0 -34.9,-15.6 -34.9,-34.9c0,-19.3 15.6,-34.9 34.9,-34.9c19.3,0 34.9,15.6 34.9,34.9C234.9,380.1 219.3,395.8 200,395.8"
android:fillColor="#E535AB"/>
<path
android:pathData="M200,74c-19.3,0 -34.9,-15.6 -34.9,-34.9c0,-19.3 15.6,-34.9 34.9,-34.9c19.3,0 34.9,15.6 34.9,34.9C234.9,58.4 219.3,74 200,74"
android:fillColor="#E535AB"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,17 @@
android:id="@+id/url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/pluto___margin_small"
android:layout_marginStart="@dimen/pluto___margin_mini"
app:layout_goneMarginStart="@dimen/pluto___margin_small"
android:layout_marginTop="@dimen/pluto___margin_medium"
android:layout_marginLeft="@dimen/pluto___margin_small"
android:fontFamily="@font/muli_semibold"
android:textColor="@color/pluto___text_dark"
android:textSize="@dimen/pluto___text_small"
android:layout_marginEnd="@dimen/pluto___margin_mini"
app:layout_constraintStart_toEndOf="@+id/status"
app:layout_constraintStart_toEndOf="@+id/graphqlIcon"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginRight="@dimen/pluto___margin_mini"
app:layout_constraintEnd_toStartOf="@+id/proxyIndicator"
tools:text="api endpoint" />
tools:text="POST /api/v2" />

<ImageView
android:id="@+id/proxyIndicator"
Expand All @@ -74,20 +73,31 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/url" />

<ImageView
android:id="@+id/graphqlIcon"
android:layout_width="@dimen/pluto___text_small"
android:layout_height="@dimen/pluto___text_small"
android:layout_marginStart="@dimen/pluto___margin_small"
android:src="@drawable/pluto_network___ic_graphql"
app:layout_constraintBottom_toBottomOf="@id/url"
app:layout_constraintStart_toEndOf="@id/status"
app:layout_constraintTop_toTopOf="@id/url" />

<TextView
android:id="@+id/host"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/pluto___margin_micro"
android:layout_marginEnd="@dimen/pluto___margin_small"
android:layout_marginBottom="@dimen/pluto___margin_medium"
android:layout_marginStart="@dimen/pluto___margin_small"
android:ellipsize="end"
android:fontFamily="@font/muli"
android:textColor="@color/pluto___text_dark_60"
android:textSize="@dimen/pluto___text_xsmall"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/timeElapsed"
app:layout_constraintStart_toStartOf="@+id/url"
app:layout_constraintStart_toEndOf="@+id/status"
app:layout_constraintTop_toBottomOf="@+id/url"
android:layout_marginRight="@dimen/pluto___margin_small"
tools:text="https host" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ private val saveAttributeKey = AttributeKey<Unit>("ResponseBodySaved")
fun HttpClient.addPlutoKtorInterceptor() {
plugin(HttpSend).intercept { requestUnBuilt ->
val request = requestUnBuilt.build()
val networkInterceptor = NetworkInterceptor.intercept(request.convert(), NetworkInterceptor.Option(NAME))
val convertedRequest = request.convert()
val networkInterceptor = NetworkInterceptor.intercept(convertedRequest, NetworkInterceptor.Option(NAME))
val callResult = try {
requestUnBuilt.url(networkInterceptor.actualOrMockRequestUrl)
execute(requestUnBuilt)
Expand All @@ -34,7 +35,7 @@ fun HttpClient.addPlutoKtorInterceptor() {
newCall.attributes.put(saveAttributeKey, Unit)
newCall
}
networkInterceptor.onResponse(res.response.convert())
networkInterceptor.onResponse(res.response.convert(convertedRequest))
res
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.pluto.plugins.network.ktor.internal

import com.pluto.plugins.network.intercept.NetworkData
import com.pluto.plugins.network.intercept.NetworkData.Body
import com.pluto.plugins.network.intercept.NetworkData.Response
import io.ktor.client.statement.HttpResponse
Expand All @@ -9,15 +10,16 @@ import io.ktor.http.Headers
import io.ktor.http.contentType

internal object KtorResponseConverter : ResponseConverter<HttpResponse> {
override suspend fun HttpResponse.convert(): Response {
override suspend fun HttpResponse.convert(request: NetworkData.Request): Response {
return Response(
request = request,
statusCode = status.value,
body = extractBody(),
protocol = version.name,
fromDiskCache = false,
headers = headersMap(headers),
sentTimestamp = requestTime.timestamp,
receiveTimestamp = responseTime.timestamp
receiveTimestamp = responseTime.timestamp,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.pluto.plugins.network.ktor.internal

import com.pluto.plugins.network.intercept.NetworkData
import com.pluto.plugins.network.intercept.NetworkData.Response

internal interface ResponseConverter<T> {
suspend fun T.convert(): Response
suspend fun T.convert(request: NetworkData.Request): Response
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,27 @@ class PlutoOkhttpInterceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val networkInterceptor = NetworkInterceptor.intercept(request.convert(), NetworkInterceptor.Option(NAME))
val convertedRequest = request.convert()
val networkInterceptor = NetworkInterceptor.intercept(convertedRequest, NetworkInterceptor.Option(NAME))
val response: Response = try {
val builder = request.newBuilder().url(networkInterceptor.actualOrMockRequestUrl)
chain.proceed(builder.build())
} catch (e: IOException) {
networkInterceptor.onError(e)
throw e
}
return response.processBody { networkInterceptor.onResponse(it) }
return response.processBody(convertedRequest) { networkInterceptor.onResponse(it) }
}
}
}

private fun Response.processBody(onComplete: (NetworkData.Response) -> Unit): Response {
private fun Response.processBody(request: NetworkData.Request, onComplete: (NetworkData.Response) -> Unit): Response {
if (!hasBody()) {
onComplete.invoke(convert(null))
onComplete.invoke(convert(request, null))
return this
}
val responseBody: ResponseBody = body as ResponseBody
val sideStream = ReportingSink(PlutoInterface.files.createFile(), ResponseReportingSinkCallback(this, onComplete))
val sideStream = ReportingSink(PlutoInterface.files.createFile(), ResponseReportingSinkCallback(this, request, onComplete))
val processedResponseBody: ResponseBody = DepletingSource(TeeSource(responseBody.source(), sideStream))
.buffer()
.asResponseBody(responseBody)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ internal fun Request.headerMap(contentLength: Long): Map<String, String?> {
return map
}

internal fun Response.convert(body: NetworkData.Body?): NetworkData.Response {
internal fun Response.convert(request: NetworkData.Request, body: NetworkData.Body?): NetworkData.Response {
return NetworkData.Response(
request = request,
statusCode = code,
body = body,
protocol = protocol.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import java.io.IOException

class ResponseReportingSinkCallback(
private val response: Response,
private val request: NetworkData.Request,
private val onComplete: (NetworkData.Response) -> Unit
) : ReportingSink.Callback {

Expand All @@ -20,7 +21,7 @@ class ResponseReportingSinkCallback(
readResponseBuffer(f, response.isGzipped)?.let {
val responseBody = response.body ?: return
val body = responseBody.processBody(it)
onComplete.invoke(response.convert(body))
onComplete.invoke(response.convert(request, body))
}
f.delete()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class DemoNetworkFragment : Fragment(R.layout.fragment_demo_network) {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.graphqlQuery.setOnClickListener { okhttpViewModel.graphqlQuery() }
binding.graphqlQueryError.setOnClickListener { okhttpViewModel.graphqlQueryError() }
binding.graphqlMutation.setOnClickListener { okhttpViewModel.graphqlMutation() }
binding.graphqlMutationError.setOnClickListener { okhttpViewModel.graphqlMutationError() }
binding.postCall.setOnClickListener { okhttpViewModel.post() }
binding.getCall.setOnClickListener { okhttpViewModel.get() }
binding.getCallKtor.setOnClickListener { ktorViewModel.get() }
Expand Down
Loading
Loading