-
Notifications
You must be signed in to change notification settings - Fork 14
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
implement a converter to dynamic message #212
implement a converter to dynamic message #212
Conversation
…ment-converter-to-dynamic-message
inline fun <reified T : KtMessage> Any.isA() = | ||
typeUrl.substringAfterLast('/') == | ||
( | ||
T::class.java.getAnnotation(KtGeneratedMessage::class.java)?.fullTypeName | ||
?: T::class.java.getAnnotation(com.toasttab.protokt.rt.KtGeneratedMessage::class.java)?.fullTypeName |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was removed a few PRs ago so it's dead code
fun KtMessage.toDynamicMessage(context: RuntimeContext): DynamicMessage = | ||
context.convertValue(this) as DynamicMessage | ||
|
||
fun KtMessage.hasField(field: FieldDescriptor): Boolean { | ||
val value = getField(field) | ||
|
||
return if (field.hasPresence()) { | ||
value != null | ||
} else { | ||
value != defaultValue(field) | ||
} | ||
} | ||
|
||
private fun defaultValue(field: FieldDescriptor) = | ||
when (field.type) { | ||
Type.UINT64, Type.FIXED64 -> 0uL | ||
Type.UINT32, Type.FIXED32 -> 0u | ||
Type.BYTES -> Bytes.empty() | ||
else -> field.defaultValue | ||
} | ||
|
||
fun KtMessage.getField(field: FieldDescriptor) = | ||
ProtoktReflect.getField(this, field) | ||
|
||
class RuntimeContext internal constructor( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New public APIs. Open to revision before 1.0.
import com.squareup.kotlinpoet.CodeBlock | ||
import com.squareup.kotlinpoet.MemberName | ||
import com.squareup.kotlinpoet.MemberName.Companion.member | ||
import com.squareup.kotlinpoet.asTypeName |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removing kotlinpoet deps from code now used in reflection at runtime
import protokt.v1.codegen.util.KotlinPlugin | ||
import protokt.v1.codegen.util.Message | ||
import protokt.v1.codegen.util.Oneof | ||
import protokt.v1.codegen.util.StandardField | ||
import protokt.v1.codegen.util.Tag | ||
import protokt.v1.reflect.FieldType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these types that are used for codegen but are also going to be used in dynamic conversion are moved to shared source and stripped of any bad runtime deps, e.g. KotlinPoet. They're staying internal
so the public API does not expose them.
@@ -30,3 +36,30 @@ class ProtoktCodegenTest : AbstractProtoktCodegenTest() { | |||
} | |||
} | |||
} | |||
|
|||
object UuidBytesConverter : AbstractConverter<Bytes, UUID>(), OptimizedSizeOfConverter<Bytes, UUID> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow testing of wrapper types with a debugger.
srcDir(rootProject.file("shared-src/reflect")) | ||
} | ||
proto { | ||
srcDir("../extensions/protokt-extensions-lite/src/extensions-proto") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generate the protokt extensions with Java for this module to be able to load its descriptor to get our custom options in the reflect runtime.
fun KtMessage.hasField(field: FieldDescriptor): Boolean { | ||
val value = getField(field) | ||
|
||
return if (field.hasPresence()) { | ||
value != null | ||
} else { | ||
value != defaultValue(field) | ||
} | ||
} | ||
|
||
private fun defaultValue(field: FieldDescriptor) = | ||
when (field.type) { | ||
Type.UINT64, Type.FIXED64 -> 0uL | ||
Type.UINT32, Type.FIXED32 -> 0u | ||
Type.BYTES -> Bytes.empty() | ||
else -> field.defaultValue | ||
} | ||
|
||
fun KtMessage.getField(field: FieldDescriptor) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasField
and getField
are the two APIs needed to run the Buf validator.
}.run { | ||
val table = HashBasedTable.create<String, String, MutableList<Converter<*, *>>>() | ||
forEach { table.getOrPut(it.wrapped.qualifiedName!!, it.wrapper.qualifiedName!!) { mutableListOf() }.add(it) } | ||
ImmutableTable.builder<String, String, List<Converter<*, *>>>().putAll(table).build() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing Guava from the reflect runtime's dependencies.
|
||
package protokt.v1.reflect | ||
|
||
internal fun inferClassName(className: String, pkg: String): Pair<String, List<String>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are more or less copied from KotlinPoet since we need them for reflection.
data object SInt64 : Scalar(Long::class) | ||
data object UInt32 : Scalar(UInt::class) | ||
data object UInt64 : Scalar(ULong::class) | ||
object Bool : Scalar(Boolean::class) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need compatibility with Kotlin 1.8
sourceSets { | ||
test { | ||
java { | ||
srcDir(rootProject.file("shared-src/lite-util")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
forgotten edge case - good to have the lite tests here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comments inline.
My other question is - can we just codegenerate a converter from KtMesage to DynamicMessage for each KtMessage without using any reflection?
} | ||
|
||
private fun getDescriptors() = | ||
ClassGraph() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we generate an annotation on the message class that points to the descriptor?
I feel really uncomfortable scanning the whole classpath for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps. We can't point to it directly, since it won't exist in lite code, but maybe there's a way to make reflection direct instead of scanned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay - the reason this is implemented reflectively is for convenience. If we had an annotation on a message in your hypothetical case, you still have to get those annotations, either by scanning or by explicit reference. If you have them explicitly then you may as well reference the descriptors themselves instead to build your own RuntimeContext.
If we don't want to encourage this kind of scan, which should only be done once, e.g. at application initialization to build validators, then we can remove this code and require users to write this themselves.
|
||
internal val descriptorsByTypeName = descriptors.associateBy { it.fullName } | ||
|
||
fun convertValue(value: Any?) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not obvious what this public API method does
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can make this not take a nullable argument for starters.
Would naming it better be a good idea? I think it has to be able to take any type since it's called at runtime with whatever field value the descriptor extracts from a protokt message.
A more descriptive name could be convertProtoktToProtobufJava
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason this has to be public is to enable sub-message conversion in protovalidate. If it's acceptable to convert the whole message every time, even if some portion of the message may never be validated, then this can be internal.
We could, but that feels a bit messy. I haven't benchmarked this yet but I think it's a big enough step forward in terms of permitting message validation that it's worth having. This doesn't prevent us from doing that in the future, of course. |
Code generation of a converter gets tricky - is there a way to do it without exposing a dependency on protobuf-java? |
No description provided.