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:
Harold Martin
2021-04-07 13:30:03 -07:00
committed by Facebook GitHub Bot
parent f28636b427
commit b0cbf39a1a
12 changed files with 337 additions and 0 deletions

View 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'

View 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

View File

@@ -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>

View File

@@ -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()

View 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.
*/
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
)
}
}
}

View File

@@ -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
}
}

View File

@@ -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()
}

View File

@@ -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?
)

View File

@@ -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
)

View File

@@ -102,6 +102,7 @@ ext.deps = [
okhttp3 : 'com.squareup.okhttp3:okhttp:4.9.1', okhttp3 : 'com.squareup.okhttp3:okhttp:4.9.1',
leakcanary : 'com.squareup.leakcanary:leakcanary-android:1.6.3', leakcanary : 'com.squareup.leakcanary:leakcanary-android:1.6.3',
leakcanary2 : 'com.squareup.leakcanary:leakcanary-android:2.6', leakcanary2 : 'com.squareup.leakcanary:leakcanary-android:2.6',
protobuf : 'com.google.protobuf:protobuf-java:3.15.6',
testCore : 'androidx.test:core:1.3.0', testCore : 'androidx.test:core:1.3.0',
testRules : 'androidx.test:rules:1.3.0', testRules : 'androidx.test:rules:1.3.0',
// Plugin dependencies // Plugin dependencies

View 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)
```

View File

@@ -44,3 +44,6 @@ project(':leakcanary-plugin').projectDir = file('android/plugins/leakcanary')
include ':leakcanary2-plugin' include ':leakcanary2-plugin'
project(':leakcanary2-plugin').projectDir = file('android/plugins/leakcanary2') project(':leakcanary2-plugin').projectDir = file('android/plugins/leakcanary2')
include ':retrofit2-protobuf'
project(':retrofit2-protobuf').projectDir = file('android/plugins/retrofit2-protobuf')