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: Basic GraphQL support #328

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
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,8 +12,32 @@ 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 variables: JSONObject,
)

val graphqlData: GraphqlData? = parseGraphqlData()

private fun parseGraphqlData(): GraphqlData? {
if (method != "POST" ||
body == null ||
!body.isJson
) return null
val json = runCatching { JSONObject(body!!.body.toString()) }.getOrNull() ?: return null
val query = json.optString("query") ?: return null
val variables = json.optJSONObject("variables") ?: JSONObject()
val match = graqphlQueryRegex.find(query)?.groupValues ?: return null
return GraphqlData(
queryType = match[1],
queryName = match[2],
variables = variables,
)
}

internal val isGzipped: Boolean
get() = headers["Content-Encoding"].equals("gzip", ignoreCase = true)
}
Expand All @@ -36,17 +61,19 @@ class NetworkData {

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 isJson get() = mediaTypeFull == "application/json"
}

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 @@ -5,6 +5,7 @@ import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
Expand Down Expand Up @@ -125,8 +126,16 @@ internal class DetailsFragment : Fragment(R.layout.pluto_network___fragment_deta

private val detailsObserver = Observer<DetailContentData> {
setupStatusView(it.api)
binding.method.text = it.api.request.method.uppercase()
binding.url.text = Url(it.api.request.url).toString()
val graphqlData = it.api.request.graphqlData
binding.graphqlIcon.isVisible = graphqlData != null
if (graphqlData != null) {
binding.method.text = "${graphqlData.queryType.uppercase()} ${graphqlData.queryName}"
binding.url.text = graphqlData.variables.toString()
} else {
binding.method.text = it.api.request.method.uppercase()
binding.url.text = Url(it.api.request.url).toString()
}

binding.overview.apply {
visibility = VISIBLE
set(it.api)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ internal class ListFragment : Fragment(R.layout.pluto_network___fragment_list) {
var list = emptyList<ApiCallData>()
viewModel.apiCalls.value?.let {
list = it.filter { api ->
api.request.url.toString().contains(search, true)
api.request.url.contains(search, true) ||
api.request.graphqlData?.queryName?.contains(search, true) ?: false
}
}
binding.noItemText.text = getString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ internal class OverviewStub : ConstraintLayout {
value = context.createSpan { append(semiBold(api.interceptorOption.name)) }
)
)
if (api.request.graphqlData != null) {
add(
KeyValuePairData(
key = context.getString(R.string.pluto_network___method_label),
value = api.request.method
)
)
add(
KeyValuePairData(
key = context.getString(R.string.pluto_network___url_label),
value = api.request.url
)
)
}
}
)
}
Expand Down
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 @@ -108,16 +108,30 @@
android:layout_height="wrap_content"
android:paddingBottom="@dimen/pluto___margin_medium">

<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_medium"
android:src="@drawable/pluto_network___ic_graphql"
app:layout_constraintBottom_toBottomOf="@id/method"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/method" />

<TextView
android:id="@+id/method"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/pluto___margin_medium"
android:layout_marginStart="@dimen/pluto___margin_mini"
android:layout_marginTop="@dimen/pluto___margin_medium"
android:layout_marginEnd="@dimen/pluto___margin_medium"
android:fontFamily="@font/muli_bold"
android:textColor="@color/pluto___dark"
android:textSize="@dimen/pluto___text_xmedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/graphqlIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="@dimen/pluto___margin_medium"
tools:text="POST" />

<TextView
Expand All @@ -126,7 +140,9 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/pluto___margin_medium"
android:layout_marginTop="@dimen/pluto___margin_micro"
android:ellipsize="end"
android:fontFamily="@font/muli"
android:maxLines="5"
android:textColor="@color/pluto___dark_60"
android:textSize="@dimen/pluto___text_xmedium"
app:layout_constraintTop_toBottomOf="@+id/method"
Expand Down
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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ interface ApiService {
)
@POST("xml")
suspend fun xml(@Body hashMapOf: RequestBody): Any

// https://studio.apollographql.com/public/SpaceX-pxxbxen/variant/current/home
@POST("https://spacex-production.up.railway.app/")
suspend fun graphql(@Body body: Any): Any
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,58 @@ class OkhttpViewModel : ViewModel() {
}
}

fun graphqlQuery() {
viewModelScope.launch {
enqueue {
apiService.graphql(
mapOf(
GQL_QUERY to "query Launches(\$limit: Int){launches(limit: \$limit){mission_name}}",
GQL_VARIABLES to mapOf("limit" to GQL_LIMIT_VALID),
)
)
}
}
}

fun graphqlQueryError() {
viewModelScope.launch {
enqueue {
apiService.graphql(
mapOf(
GQL_QUERY to "query Launches(\$limit: Int){launches(limit: \$limit){mission_name}}",
GQL_VARIABLES to mapOf("limit" to GQL_LIMIT_INVALID),
)
)
}
}
}

fun graphqlMutation() {
viewModelScope.launch {
enqueue {
apiService.graphql(
mapOf(
GQL_QUERY to "mutation Insert_users(\$objects: [users_insert_input!]!) {insert_users(objects: \$objects) {affected_rows}}",
GQL_VARIABLES to mapOf("objects" to emptyList<Any>()),
)
)
}
}
}

fun graphqlMutationError() {
viewModelScope.launch {
enqueue {
apiService.graphql(
mapOf(
GQL_QUERY to "mutation Insert_users(\$objects: [users_insert_input!]!) {insert_users112231321(objects: \$objects) {affected_rows}}",
GQL_VARIABLES to mapOf("objects" to emptyList<Any>()),
)
)
}
}
}

fun post() {
val label = "POST call"
viewModelScope.launch {
Expand Down Expand Up @@ -79,4 +131,11 @@ class OkhttpViewModel : ViewModel() {
)
}
}

companion object {
private const val GQL_QUERY = "query"
private const val GQL_LIMIT_VALID = 3
private const val GQL_LIMIT_INVALID = -1111
private const val GQL_VARIABLES = "variables"
}
}
Loading
Loading