-
Notifications
You must be signed in to change notification settings - Fork 624
Create types to represent parsed protobuf messages. #2371
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
162 changes: 162 additions & 0 deletions
162
...gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Types.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// Copyright 2021 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package com.google.firebase.encoders.proto.codegen | ||
|
||
/** Represents a protocol buffer type. */ | ||
sealed class ProtobufType { | ||
abstract val javaName: String | ||
} | ||
|
||
/** | ||
* Primitive protocol buffer type. | ||
* | ||
* A set of all possible primitive types is defined below. | ||
* | ||
* @property javaName Java type name that corresponds to a given protobuf primitive type. | ||
* @property defaultValue Default literal value of a primitive type according to the proto3 spec. | ||
*/ | ||
sealed class Primitive(override val javaName: String, val defaultValue: String) : ProtobufType() { | ||
override fun toString(): String = this::class.java.simpleName | ||
|
||
object INT32 : Primitive("int", "0") | ||
object SINT32 : Primitive("int", "0") | ||
object FIXED32 : Primitive("int", "0") | ||
object SFIXED32 : Primitive("int", "0") | ||
object FLOAT : Primitive("float", "0") | ||
|
||
object INT64 : Primitive("long", "0") | ||
object SINT64 : Primitive("long", "0") | ||
object FIXED64 : Primitive("long", "0") | ||
object SFIXED64 : Primitive("long", "0") | ||
object DOUBLE : Primitive("double", "0") | ||
|
||
object BOOLEAN : Primitive("boolean", "false") | ||
object STRING : Primitive("String", "\"\"") | ||
object BYTES : Primitive("byte[]", "new byte[0]") | ||
} | ||
|
||
/** | ||
* User defined type. | ||
* | ||
* Can be one of [Message] or [ProtoEnum]. | ||
*/ | ||
sealed class UserDefined : ProtobufType() { | ||
/** A fully qualified protobuf message name, i.e. `.com.example.Outer.Inner`. */ | ||
abstract val protobufFullName: String | ||
|
||
/** Specifies the scope that this type is defined in, i.e. a [Owner.Package] or a parent [Message]. */ | ||
abstract val owner: Owner | ||
|
||
/** Unqualified name of this type */ | ||
abstract val name: String | ||
|
||
/** A fully qualified java name of this type, i.e. `com.example.Outer$Inner`. */ | ||
override val javaName: String | ||
get() = "${owner.javaName}${owner.scopeSeparator}$name" | ||
|
||
/** Represents a protobuf `message` type. */ | ||
data class Message( | ||
override val owner: Owner, | ||
override val name: String, | ||
val fields: List<ProtoField> | ||
) : UserDefined() { | ||
override val protobufFullName: String | ||
get() = "${owner.protobufFullName}.$name" | ||
} | ||
|
||
/** Represents a protobuf `enum` type. */ | ||
data class ProtoEnum(override val owner: Owner, override val name: String, val values: List<Value>) : UserDefined() { | ||
override val protobufFullName: String | ||
get() = "${owner.protobufFullName}.$name" | ||
|
||
/** Represents possible enum values including name and field number. */ | ||
data class Value(val name: String, val value: Int) | ||
} | ||
} | ||
|
||
/** | ||
* Represent a not yet resolved type, only its fully qualified name is known. | ||
* | ||
* This type is required during the parsing process. The issue is that when fields are parsed they | ||
* can reference types that have not themselves been parsed yet. Additionally reference cycles are | ||
* possible, when a message `A` has a field of type `A` either directly or transitively. | ||
* | ||
* To address that all non-primitive fields are initially set to [Unresolved], and once all messages | ||
* are parsed, all unresolved references are replaced with their respective | ||
* [protobuf types][protobufName]. | ||
* | ||
* @property protobufName Fully-qualified protobuf name of the message/enum. | ||
*/ | ||
data class Unresolved(val protobufName: String) : ProtobufType() { | ||
override val javaName: String | ||
get() = | ||
throw UnsupportedOperationException( | ||
"Unresolved types don't have a javaName, they are intended to be resolved " + | ||
"after parsing is complete." | ||
) | ||
} | ||
|
||
/** | ||
* Own user define types. | ||
* | ||
* According to the protocol buffer language definition, user defined types can either be | ||
* file-scoped or nested within another [UserDefined.Message]. Hence the only possible owners are | ||
* [Owner.Package] or [UserDefined.Message] (represented by [Owner.MsgRef]) respectively. | ||
*/ | ||
sealed class Owner(val scopeSeparator: Char) { | ||
abstract val protobufFullName: String | ||
abstract val fileName: String | ||
abstract val javaName: String | ||
|
||
/** Represents a package that a protobuf type belongs to. */ | ||
data class Package(val name: String, val javaPackage: String, override val fileName: String) : Owner('.') { | ||
override val protobufFullName: String | ||
get() = ".$name" | ||
|
||
override val javaName: String | ||
get() = javaPackage | ||
} | ||
|
||
/** Represents a message that contains nested protobuf types. */ | ||
data class MsgRef(val message: UserDefined.Message) : Owner('$') { | ||
override val protobufFullName: String | ||
get() = message.protobufFullName | ||
override val fileName: String | ||
get() = message.owner.fileName | ||
override val javaName: String | ||
get() = message.javaName | ||
|
||
override fun toString(): String = "MsgRef(name=${message.protobufFullName})" | ||
} | ||
} | ||
|
||
/** | ||
* Represents a field of a protobuf message. | ||
* | ||
* @property name name of the field as defined in the proto file, usually camel_cased. | ||
* @property type this property is mutable because it's not always possible specify the type | ||
* upfront. See [Unresolved] for details. | ||
* */ | ||
data class ProtoField(val name: String, var type: ProtobufType, val number: Int, val repeated: Boolean = false) { | ||
/** Custom toString() needed to avoid stackoverflow if case of message reference cycles. */ | ||
override fun toString(): String = "ProtoField(\"$name\":$number, ${ | ||
type.let { | ||
when (it) { | ||
is UserDefined.Message -> it.protobufFullName | ||
else -> it.toString() | ||
} | ||
} | ||
})" | ||
} |
94 changes: 94 additions & 0 deletions
94
...firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/TypeTests.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// Copyright 2021 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package com.google.firebase.encoders.proto.codegen | ||
|
||
import com.google.common.truth.Truth.assertThat | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
import org.junit.runners.JUnit4 | ||
|
||
@RunWith(JUnit4::class) | ||
class TypeTests { | ||
@Test | ||
fun `proto and java qualified names work as expected`() { | ||
val package1 = Owner.Package("com.example", "com.example.proto", "my_proto.proto") | ||
val helloMsg = UserDefined.Message(owner = package1, name = "Hello", fields = listOf()) | ||
val worldMsg = UserDefined.Message(Owner.MsgRef(helloMsg), "World", fields = listOf()) | ||
|
||
assertThat(helloMsg.protobufFullName).isEqualTo(".com.example.Hello") | ||
assertThat(helloMsg.javaName).isEqualTo("com.example.proto.Hello") | ||
|
||
assertThat(worldMsg.protobufFullName).isEqualTo(".com.example.Hello.World") | ||
assertThat(worldMsg.javaName).isEqualTo("com.example.proto.Hello\$World") | ||
} | ||
|
||
@Test | ||
fun `ProtoField#toString() method should not cause stack overflow`() { | ||
/* | ||
syntax = "proto3"; | ||
package com.example; | ||
option java_package = "com.example.proto"; | ||
|
||
message Hello { | ||
message World { | ||
Hello hello = 1; | ||
int64 my_long = 2; | ||
} | ||
World world = 1; | ||
repeated int32 my_int = 2; | ||
} | ||
*/ | ||
val package1 = Owner.Package("com.example", "com.example.proto", "my_proto.proto") | ||
val worldField = ProtoField( | ||
name = "world", | ||
type = Unresolved(".com.example.Hello.World"), | ||
number = 1 | ||
) | ||
val helloField = ProtoField( | ||
name = "hello", | ||
type = Unresolved(".com.example.Hello"), | ||
number = 1 | ||
) | ||
val helloMsg = UserDefined.Message( | ||
owner = package1, | ||
name = "Hello", | ||
fields = listOf( | ||
worldField, | ||
ProtoField( | ||
name = "my_int", | ||
type = Primitive.INT32, | ||
number = 2, | ||
repeated = true | ||
) | ||
) | ||
) | ||
val worldMsg = UserDefined.Message( | ||
owner = Owner.MsgRef(helloMsg), | ||
name = "World", | ||
fields = listOf( | ||
helloField, | ||
ProtoField( | ||
name = "my_long", | ||
type = Primitive.INT64, | ||
number = 2 | ||
) | ||
) | ||
) | ||
worldField.type = worldMsg | ||
helloField.type = helloMsg | ||
|
||
assertThat(helloMsg.fields[0].toString()).contains(".com.example.Hello.World") | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.