diff --git a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/intercept/NetworkData.kt b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/intercept/NetworkData.kt index d9572b05..236ba76c 100644 --- a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/intercept/NetworkData.kt +++ b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/intercept/NetworkData.kt @@ -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 { @@ -11,8 +12,32 @@ class NetworkData { val method: String, val body: Body?, val headers: Map, - 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) } @@ -36,7 +61,7 @@ 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 @@ -44,9 +69,11 @@ class NetworkData { 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+)""") } } diff --git a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/DetailsFragment.kt b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/DetailsFragment.kt index 4e530c05..457df78f 100644 --- a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/DetailsFragment.kt +++ b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/DetailsFragment.kt @@ -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 @@ -125,8 +126,16 @@ internal class DetailsFragment : Fragment(R.layout.pluto_network___fragment_deta private val detailsObserver = Observer { 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) diff --git a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ListFragment.kt b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ListFragment.kt index 21f43630..f3a09735 100644 --- a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ListFragment.kt +++ b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ListFragment.kt @@ -74,7 +74,8 @@ internal class ListFragment : Fragment(R.layout.pluto_network___fragment_list) { var list = emptyList() 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( diff --git a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/components/OverviewStub.kt b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/components/OverviewStub.kt index f66de721..06369fc8 100644 --- a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/components/OverviewStub.kt +++ b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/components/OverviewStub.kt @@ -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 + ) + ) + } } ) } diff --git a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/list/ApiItemHolder.kt b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/list/ApiItemHolder.kt index c9dea7ec..efeacf82 100644 --- a/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/list/ApiItemHolder.kt +++ b/pluto-plugins/plugins/network/core/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/list/ApiItemHolder.kt @@ -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 @@ -30,6 +31,7 @@ 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) { @@ -37,9 +39,13 @@ internal class ApiItemHolder(parent: ViewGroup, actionListener: DiffAwareAdapter 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 diff --git a/pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_graphql.xml b/pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_graphql.xml new file mode 100644 index 00000000..e1f69254 --- /dev/null +++ b/pluto-plugins/plugins/network/core/lib/src/main/res/drawable/pluto_network___ic_graphql.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/pluto-plugins/plugins/network/core/lib/src/main/res/layout/pluto_network___fragment_details.xml b/pluto-plugins/plugins/network/core/lib/src/main/res/layout/pluto_network___fragment_details.xml index 4c267749..563b1a4a 100644 --- a/pluto-plugins/plugins/network/core/lib/src/main/res/layout/pluto_network___fragment_details.xml +++ b/pluto-plugins/plugins/network/core/lib/src/main/res/layout/pluto_network___fragment_details.xml @@ -108,16 +108,30 @@ android:layout_height="wrap_content" android:paddingBottom="@dimen/pluto___margin_medium"> + + + tools:text="POST /api/v2" /> + + diff --git a/sample/src/main/java/com/sampleapp/functions/network/DemoNetworkFragment.kt b/sample/src/main/java/com/sampleapp/functions/network/DemoNetworkFragment.kt index e0825e9f..be36d2a6 100644 --- a/sample/src/main/java/com/sampleapp/functions/network/DemoNetworkFragment.kt +++ b/sample/src/main/java/com/sampleapp/functions/network/DemoNetworkFragment.kt @@ -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() } diff --git a/sample/src/main/java/com/sampleapp/functions/network/internal/okhttp/ApiService.kt b/sample/src/main/java/com/sampleapp/functions/network/internal/okhttp/ApiService.kt index cc7e6258..556d070d 100644 --- a/sample/src/main/java/com/sampleapp/functions/network/internal/okhttp/ApiService.kt +++ b/sample/src/main/java/com/sampleapp/functions/network/internal/okhttp/ApiService.kt @@ -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 } diff --git a/sample/src/main/java/com/sampleapp/functions/network/internal/okhttp/OkhttpViewModel.kt b/sample/src/main/java/com/sampleapp/functions/network/internal/okhttp/OkhttpViewModel.kt index f6a0584f..b03e58af 100644 --- a/sample/src/main/java/com/sampleapp/functions/network/internal/okhttp/OkhttpViewModel.kt +++ b/sample/src/main/java/com/sampleapp/functions/network/internal/okhttp/OkhttpViewModel.kt @@ -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()), + ) + ) + } + } + } + + 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()), + ) + ) + } + } + } + fun post() { val label = "POST call" viewModelScope.launch { @@ -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" + } } diff --git a/sample/src/main/res/layout/fragment_container.xml b/sample/src/main/res/layout/fragment_container.xml index e9de71c6..e1c13520 100644 --- a/sample/src/main/res/layout/fragment_container.xml +++ b/sample/src/main/res/layout/fragment_container.xml @@ -14,7 +14,7 @@ + + + + + + + + + \ No newline at end of file