Export protobuf definitions from Retrofit (#2084)
Summary: https://github.com/facebook/flipper/pull/2080 enables storing protobuf definitions and displaying payloads in the network inspector plugin. However, describing those definitions in ProtobufJS format can be time consuming and error prone. This PR enables sending definitions from an entire Retrofit service with a single line. ## Changelog * Adds a retrofit2-protobuf plugin Pull Request resolved: https://github.com/facebook/flipper/pull/2084 Test Plan: Used as a single line per service, ie: `SendProtobufToFlipperFromRetrofit(baseUrl, PersonService::class.java)` For more details see demo app in https://github.com/hbmartin/protobuf_java_to_protobufjs Reviewed By: mweststrate Differential Revision: D27507872 Pulled By: passy fbshipit-source-id: 859d7636c9512de0abde0aa1dcb2e023851369cf
This commit is contained in:
committed by
Facebook GitHub Bot
parent
f28636b427
commit
b0cbf39a1a
34
android/plugins/retrofit2-protobuf/build.gradle
Normal file
34
android/plugins/retrofit2-protobuf/build.gradle
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'maven'
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.compileSdkVersion
|
||||
buildToolsVersion rootProject.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.minSdkVersion
|
||||
targetSdkVersion rootProject.targetSdkVersion
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
|
||||
implementation project(':android')
|
||||
implementation project(':network-plugin')
|
||||
implementation deps.protobuf
|
||||
implementation "com.squareup.retrofit2:retrofit:2.9.0"
|
||||
implementation "com.github.hbmartin:protobuf_java_to_protobufjs:0.0.1"
|
||||
compileOnly deps.jsr305
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.vanniktech.maven.publish'
|
||||
12
android/plugins/retrofit2-protobuf/gradle.properties
Normal file
12
android/plugins/retrofit2-protobuf/gradle.properties
Normal file
@@ -0,0 +1,12 @@
|
||||
#
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the LICENSE
|
||||
# file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
POM_NAME=Retrofit2 Protobuf Adapter
|
||||
POM_DESCRIPTION=Flipper plugin to automate Retrofit2 sending Protobuf definitions
|
||||
POM_ARTIFACT_ID=flipper-retrofit2-protobuf-plugin
|
||||
POM_PACKAGING=aar
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) Facebook, Inc. and its affiliates.
|
||||
~
|
||||
~ This source code is licensed under the MIT license found in the LICENSE
|
||||
~ file in the root directory of this source tree.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.facebook.flipper.plugins.retrofit2protobuf">
|
||||
</manifest>
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.flipper.plugins.retrofit2protobuf
|
||||
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.core.FlipperArray
|
||||
import com.facebook.flipper.core.FlipperObject
|
||||
import com.facebook.flipper.core.FlipperValue
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
|
||||
import com.facebook.flipper.plugins.retrofit2protobuf.adapter.GenericCallDefinitionsToMessageDefinitionsIfProtobuf
|
||||
import com.facebook.flipper.plugins.retrofit2protobuf.adapter.RetrofitServiceToGenericCallDefinitions
|
||||
import com.facebook.flipper.plugins.retrofit2protobuf.model.CallNestedMessagesPayload
|
||||
import me.haroldmartin.protobufjavatoprotobufjs.adapter.FullNamedMessagesToNestedMessages
|
||||
|
||||
object SendProtobufToFlipperFromRetrofit {
|
||||
operator fun invoke(baseUrl: String, service: Class<*>) {
|
||||
AndroidFlipperClient.getInstanceIfInitialized()?.let { client ->
|
||||
(client.getPlugin(NetworkFlipperPlugin.ID) as? NetworkFlipperPlugin)
|
||||
?.send(
|
||||
"addProtobufDefinitions",
|
||||
FlipperObject.Builder().put(
|
||||
baseUrl, generateProtobufDefinitions(service).toFlipperArray()
|
||||
).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateProtobufDefinitions(service: Class<*>): List<CallNestedMessagesPayload> {
|
||||
return RetrofitServiceToGenericCallDefinitions(service).let { definitions ->
|
||||
GenericCallDefinitionsToMessageDefinitionsIfProtobuf(definitions)
|
||||
}.let { messages ->
|
||||
messages.map {
|
||||
CallNestedMessagesPayload(
|
||||
path = it.path,
|
||||
method = it.method,
|
||||
requestMessageFullName = it.requestMessageFullName,
|
||||
requestDefinitions = FullNamedMessagesToNestedMessages(it.requestModel),
|
||||
responseMessageFullName = it.responseMessageFullName,
|
||||
responseDefinitions = FullNamedMessagesToNestedMessages(it.responseModel)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Iterable<FlipperValue>.toFlipperArray(): FlipperArray =
|
||||
fold(FlipperArray.Builder()) { builder, call -> builder.put(call) }.build()
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.flipper.plugins.retrofit2protobuf.adapter
|
||||
|
||||
import com.facebook.flipper.plugins.retrofit2protobuf.model.FullNamedMessagesCallDefinition
|
||||
import com.facebook.flipper.plugins.retrofit2protobuf.model.GenericCallDefinition
|
||||
import me.haroldmartin.protobufjavatoprotobufjs.ProtobufGeneratedJavaToProtobufJs
|
||||
|
||||
internal object GenericCallDefinitionsToMessageDefinitionsIfProtobuf {
|
||||
operator fun invoke(callDefinitions: List<GenericCallDefinition>): List<FullNamedMessagesCallDefinition> {
|
||||
return callDefinitions.mapNotNull { definition ->
|
||||
val responseRootAndMessages = definition.responseType?.let {
|
||||
ProtobufGeneratedJavaToProtobufJs(it)
|
||||
}
|
||||
val requestRootAndMessages = definition.requestType?.let {
|
||||
ProtobufGeneratedJavaToProtobufJs(it)
|
||||
}
|
||||
|
||||
FullNamedMessagesCallDefinition(
|
||||
path = definition.path,
|
||||
method = definition.method,
|
||||
responseMessageFullName = responseRootAndMessages?.rootFullName,
|
||||
responseModel = responseRootAndMessages?.messages,
|
||||
requestMessageFullName = requestRootAndMessages?.rootFullName,
|
||||
requestModel = requestRootAndMessages?.messages
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.flipper.plugins.retrofit2protobuf.adapter
|
||||
|
||||
import com.facebook.flipper.plugins.retrofit2protobuf.model.GenericCallDefinition
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
internal object RetrofitServiceToGenericCallDefinitions {
|
||||
@Suppress("LoopWithTooManyJumpStatements")
|
||||
operator fun invoke(service: Class<*>): List<GenericCallDefinition> {
|
||||
val methodToProtobufDefinition = mutableListOf<GenericCallDefinition>()
|
||||
for (method in service.declaredMethods) {
|
||||
val responseType = method.innerGenericReturnClass ?: continue
|
||||
val (path, httpMethod) = method.annotations.urlPathAndMethod ?: continue
|
||||
methodToProtobufDefinition.add(
|
||||
GenericCallDefinition(
|
||||
path = path,
|
||||
method = httpMethod,
|
||||
responseType = responseType,
|
||||
requestType = method.requestBodyType
|
||||
)
|
||||
)
|
||||
}
|
||||
return methodToProtobufDefinition
|
||||
}
|
||||
}
|
||||
|
||||
private val Array<Annotation>.urlPathAndMethod: Pair<String, String>?
|
||||
get() {
|
||||
var path: Pair<String, String>? = null
|
||||
for (a in this) {
|
||||
path = when (a.annotationClass) {
|
||||
retrofit2.http.DELETE::class -> (a as retrofit2.http.DELETE).value to "DELETE"
|
||||
retrofit2.http.GET::class -> (a as retrofit2.http.GET).value to "GET"
|
||||
retrofit2.http.HEAD::class -> (a as retrofit2.http.HEAD).value to "HEAD"
|
||||
retrofit2.http.OPTIONS::class -> (a as retrofit2.http.OPTIONS).value to "OPTIONS"
|
||||
retrofit2.http.PATCH::class -> (a as retrofit2.http.PATCH).value to "PATCH"
|
||||
retrofit2.http.POST::class -> (a as retrofit2.http.POST).value to "POST"
|
||||
retrofit2.http.PUT::class -> (a as retrofit2.http.PUT).value to "PUT"
|
||||
else -> null
|
||||
}
|
||||
if (path != null) break
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
private val Method.requestBodyType: Class<*>?
|
||||
get() {
|
||||
parameterAnnotations.forEachIndexed { index, parameters ->
|
||||
parameters.forEach { annotation ->
|
||||
if (annotation.annotationClass == retrofit2.http.Body::class) {
|
||||
return parameterTypes[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private val Method.innerGenericReturnClass: Class<*>?
|
||||
get() = (genericReturnType as? ParameterizedType)?.innerGenericType as? Class<*>
|
||||
|
||||
private val ParameterizedType?.innerGenericType: Type?
|
||||
get() {
|
||||
val innerType = this?.actualTypeArguments?.get(0)
|
||||
return if (innerType is ParameterizedType) {
|
||||
innerType.innerGenericType
|
||||
} else {
|
||||
innerType
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.flipper.plugins.retrofit2protobuf.model
|
||||
|
||||
import com.facebook.flipper.core.FlipperObject
|
||||
import com.facebook.flipper.core.FlipperValue
|
||||
|
||||
internal data class CallNestedMessagesPayload(
|
||||
val path: String,
|
||||
val method: String,
|
||||
val requestMessageFullName: String?,
|
||||
val requestDefinitions: Map<String, Any>,
|
||||
val responseMessageFullName: String?,
|
||||
val responseDefinitions: Map<String, Any>
|
||||
) : FlipperValue {
|
||||
override fun toFlipperObject(): FlipperObject {
|
||||
return FlipperObject.Builder()
|
||||
.put("path", path)
|
||||
.put("method", method)
|
||||
.put("requestMessageFullName", requestMessageFullName)
|
||||
.put("requestDefinitions", requestDefinitions.toFlipperObject())
|
||||
.put("responseMessageFullName", responseMessageFullName)
|
||||
.put("responseDefinitions", responseDefinitions.toFlipperObject())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<*, *>.toFlipperObject(): FlipperObject {
|
||||
val builder = FlipperObject.Builder()
|
||||
this.forEach {
|
||||
builder.put(
|
||||
it.key as String,
|
||||
if (it.value is Map<*, *>) (it.value as Map<*, *>).toFlipperObject() else it.value
|
||||
)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.flipper.plugins.retrofit2protobuf.model
|
||||
|
||||
import me.haroldmartin.protobufjavatoprotobufjs.model.FullNamedMessages
|
||||
|
||||
internal data class FullNamedMessagesCallDefinition(
|
||||
val path: String,
|
||||
val method: String,
|
||||
val requestMessageFullName: String?,
|
||||
val responseMessageFullName: String?,
|
||||
val responseModel: FullNamedMessages?,
|
||||
val requestModel: FullNamedMessages?
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.flipper.plugins.retrofit2protobuf.model
|
||||
|
||||
internal data class GenericCallDefinition(
|
||||
val path: String,
|
||||
val method: String,
|
||||
val responseType: Class<*>? = null,
|
||||
val requestType: Class<*>? = null
|
||||
)
|
||||
@@ -102,6 +102,7 @@ ext.deps = [
|
||||
okhttp3 : 'com.squareup.okhttp3:okhttp:4.9.1',
|
||||
leakcanary : 'com.squareup.leakcanary:leakcanary-android:1.6.3',
|
||||
leakcanary2 : 'com.squareup.leakcanary:leakcanary-android:2.6',
|
||||
protobuf : 'com.google.protobuf:protobuf-java:3.15.6',
|
||||
testCore : 'androidx.test:core:1.3.0',
|
||||
testRules : 'androidx.test:rules:1.3.0',
|
||||
// Plugin dependencies
|
||||
|
||||
37
docs/setup/protobuf-retrofit-plugin.mdx
Normal file
37
docs/setup/protobuf-retrofit-plugin.mdx
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
id: protobuf-retrofit-plugin
|
||||
title: Protobut + Retrofit Setup
|
||||
sidebar_label: Protobut + Retrofit
|
||||
---
|
||||
|
||||
## Gradle Dependencies
|
||||
|
||||
Ensure that you already have an explicit dependency in your application's
|
||||
`build.gradle` including the plugin dependency, e.g.
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation "com.squareup.retrofit2:retrofit:2.9.0"
|
||||
implementation "com.squareup.retrofit2:converter-protobuf:2.9.0"
|
||||
|
||||
// update version below to match latest Flipper client app
|
||||
debugImplementation "com.facebook.flipper:flipper:0.84.0"
|
||||
debugImplementation "com.facebook.flipper:flipper-network-plugin:0.84.0"
|
||||
debugImplementation "com.facebook.flipper:flipper-retrofit2-protobuf-plugin:0.84.0"
|
||||
}
|
||||
```
|
||||
|
||||
## Network Plugin Requirement
|
||||
|
||||
Ensure that `NetworkFlipperPlugin` is added as shown in the [Network setup guide](https://fbflipper.com/docs/setup/network-plugin#android)
|
||||
|
||||
## Sending Retrofit Service
|
||||
|
||||
Suppose you have a Retrofit service interface `PersonService` which has Protobuf body or return types. At the time you create your implementation, call the plugin with your `baseUrl` and service class:
|
||||
|
||||
```
|
||||
import com.facebook.flipper.plugins.retrofit2protobuf.SendProtobufToFlipperFromRetrofit
|
||||
...
|
||||
val personService = retrofit.create(PersonService::class.java)
|
||||
SendProtobufToFlipperFromRetrofit(baseUrl, PersonService::class.java)
|
||||
```
|
||||
@@ -44,3 +44,6 @@ project(':leakcanary-plugin').projectDir = file('android/plugins/leakcanary')
|
||||
|
||||
include ':leakcanary2-plugin'
|
||||
project(':leakcanary2-plugin').projectDir = file('android/plugins/leakcanary2')
|
||||
|
||||
include ':retrofit2-protobuf'
|
||||
project(':retrofit2-protobuf').projectDir = file('android/plugins/retrofit2-protobuf')
|
||||
|
||||
Reference in New Issue
Block a user