Skip to content

Commit eb74cb6

Browse files
authored
Create types to represent parsed protobuf messages. (#2371)
1 parent a89e504 commit eb74cb6

File tree

4 files changed

+262
-3
lines changed

4 files changed

+262
-3
lines changed

encoders/protoc-gen-firebase-encoders/protoc-gen-firebase-encoders.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,7 @@ dependencies {
2828
implementation "com.google.protobuf:protobuf-java:3.11.4"
2929
implementation 'com.squareup:javapoet:1.13.0'
3030
implementation 'com.google.guava:guava:30.0-jre'
31+
32+
testImplementation 'junit:junit:4.13.1'
33+
testImplementation "com.google.truth:truth:1.0.1"
3134
}

encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Main.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ fun main(args: Array<String>) {
3434
val stringWriter = StringWriter()
3535
it.printStackTrace(PrintWriter(stringWriter))
3636
CodeGeneratorResponse.newBuilder()
37-
.setError(stringWriter.toString())
38-
.build()
39-
.writeTo(System.out)
37+
.setError(stringWriter.toString())
38+
.build()
39+
.writeTo(System.out)
4040
}
4141
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.encoders.proto.codegen
16+
17+
/** Represents a protocol buffer type. */
18+
sealed class ProtobufType {
19+
abstract val javaName: String
20+
}
21+
22+
/**
23+
* Primitive protocol buffer type.
24+
*
25+
* A set of all possible primitive types is defined below.
26+
*
27+
* @property javaName Java type name that corresponds to a given protobuf primitive type.
28+
* @property defaultValue Default literal value of a primitive type according to the proto3 spec.
29+
*/
30+
sealed class Primitive(override val javaName: String, val defaultValue: String) : ProtobufType() {
31+
override fun toString(): String = this::class.java.simpleName
32+
33+
object INT32 : Primitive("int", "0")
34+
object SINT32 : Primitive("int", "0")
35+
object FIXED32 : Primitive("int", "0")
36+
object SFIXED32 : Primitive("int", "0")
37+
object FLOAT : Primitive("float", "0")
38+
39+
object INT64 : Primitive("long", "0")
40+
object SINT64 : Primitive("long", "0")
41+
object FIXED64 : Primitive("long", "0")
42+
object SFIXED64 : Primitive("long", "0")
43+
object DOUBLE : Primitive("double", "0")
44+
45+
object BOOLEAN : Primitive("boolean", "false")
46+
object STRING : Primitive("String", "\"\"")
47+
object BYTES : Primitive("byte[]", "new byte[0]")
48+
}
49+
50+
/**
51+
* User defined type.
52+
*
53+
* Can be one of [Message] or [ProtoEnum].
54+
*/
55+
sealed class UserDefined : ProtobufType() {
56+
/** A fully qualified protobuf message name, i.e. `.com.example.Outer.Inner`. */
57+
abstract val protobufFullName: String
58+
59+
/** Specifies the scope that this type is defined in, i.e. a [Owner.Package] or a parent [Message]. */
60+
abstract val owner: Owner
61+
62+
/** Unqualified name of this type */
63+
abstract val name: String
64+
65+
/** A fully qualified java name of this type, i.e. `com.example.Outer$Inner`. */
66+
override val javaName: String
67+
get() = "${owner.javaName}${owner.scopeSeparator}$name"
68+
69+
/** Represents a protobuf `message` type. */
70+
data class Message(
71+
override val owner: Owner,
72+
override val name: String,
73+
val fields: List<ProtoField>
74+
) : UserDefined() {
75+
override val protobufFullName: String
76+
get() = "${owner.protobufFullName}.$name"
77+
}
78+
79+
/** Represents a protobuf `enum` type. */
80+
data class ProtoEnum(override val owner: Owner, override val name: String, val values: List<Value>) : UserDefined() {
81+
override val protobufFullName: String
82+
get() = "${owner.protobufFullName}.$name"
83+
84+
/** Represents possible enum values including name and field number. */
85+
data class Value(val name: String, val value: Int)
86+
}
87+
}
88+
89+
/**
90+
* Represent a not yet resolved type, only its fully qualified name is known.
91+
*
92+
* This type is required during the parsing process. The issue is that when fields are parsed they
93+
* can reference types that have not themselves been parsed yet. Additionally reference cycles are
94+
* possible, when a message `A` has a field of type `A` either directly or transitively.
95+
*
96+
* To address that all non-primitive fields are initially set to [Unresolved], and once all messages
97+
* are parsed, all unresolved references are replaced with their respective
98+
* [protobuf types][protobufName].
99+
*
100+
* @property protobufName Fully-qualified protobuf name of the message/enum.
101+
*/
102+
data class Unresolved(val protobufName: String) : ProtobufType() {
103+
override val javaName: String
104+
get() =
105+
throw UnsupportedOperationException(
106+
"Unresolved types don't have a javaName, they are intended to be resolved " +
107+
"after parsing is complete."
108+
)
109+
}
110+
111+
/**
112+
* Own user define types.
113+
*
114+
* According to the protocol buffer language definition, user defined types can either be
115+
* file-scoped or nested within another [UserDefined.Message]. Hence the only possible owners are
116+
* [Owner.Package] or [UserDefined.Message] (represented by [Owner.MsgRef]) respectively.
117+
*/
118+
sealed class Owner(val scopeSeparator: Char) {
119+
abstract val protobufFullName: String
120+
abstract val fileName: String
121+
abstract val javaName: String
122+
123+
/** Represents a package that a protobuf type belongs to. */
124+
data class Package(val name: String, val javaPackage: String, override val fileName: String) : Owner('.') {
125+
override val protobufFullName: String
126+
get() = ".$name"
127+
128+
override val javaName: String
129+
get() = javaPackage
130+
}
131+
132+
/** Represents a message that contains nested protobuf types. */
133+
data class MsgRef(val message: UserDefined.Message) : Owner('$') {
134+
override val protobufFullName: String
135+
get() = message.protobufFullName
136+
override val fileName: String
137+
get() = message.owner.fileName
138+
override val javaName: String
139+
get() = message.javaName
140+
141+
override fun toString(): String = "MsgRef(name=${message.protobufFullName})"
142+
}
143+
}
144+
145+
/**
146+
* Represents a field of a protobuf message.
147+
*
148+
* @property name name of the field as defined in the proto file, usually camel_cased.
149+
* @property type this property is mutable because it's not always possible specify the type
150+
* upfront. See [Unresolved] for details.
151+
* */
152+
data class ProtoField(val name: String, var type: ProtobufType, val number: Int, val repeated: Boolean = false) {
153+
/** Custom toString() needed to avoid stackoverflow if case of message reference cycles. */
154+
override fun toString(): String = "ProtoField(\"$name\":$number, ${
155+
type.let {
156+
when (it) {
157+
is UserDefined.Message -> it.protobufFullName
158+
else -> it.toString()
159+
}
160+
}
161+
})"
162+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.encoders.proto.codegen
16+
17+
import com.google.common.truth.Truth.assertThat
18+
import org.junit.Test
19+
import org.junit.runner.RunWith
20+
import org.junit.runners.JUnit4
21+
22+
@RunWith(JUnit4::class)
23+
class TypeTests {
24+
@Test
25+
fun `proto and java qualified names work as expected`() {
26+
val package1 = Owner.Package("com.example", "com.example.proto", "my_proto.proto")
27+
val helloMsg = UserDefined.Message(owner = package1, name = "Hello", fields = listOf())
28+
val worldMsg = UserDefined.Message(Owner.MsgRef(helloMsg), "World", fields = listOf())
29+
30+
assertThat(helloMsg.protobufFullName).isEqualTo(".com.example.Hello")
31+
assertThat(helloMsg.javaName).isEqualTo("com.example.proto.Hello")
32+
33+
assertThat(worldMsg.protobufFullName).isEqualTo(".com.example.Hello.World")
34+
assertThat(worldMsg.javaName).isEqualTo("com.example.proto.Hello\$World")
35+
}
36+
37+
@Test
38+
fun `ProtoField#toString() method should not cause stack overflow`() {
39+
/*
40+
syntax = "proto3";
41+
package com.example;
42+
option java_package = "com.example.proto";
43+
44+
message Hello {
45+
message World {
46+
Hello hello = 1;
47+
int64 my_long = 2;
48+
}
49+
World world = 1;
50+
repeated int32 my_int = 2;
51+
}
52+
*/
53+
val package1 = Owner.Package("com.example", "com.example.proto", "my_proto.proto")
54+
val worldField = ProtoField(
55+
name = "world",
56+
type = Unresolved(".com.example.Hello.World"),
57+
number = 1
58+
)
59+
val helloField = ProtoField(
60+
name = "hello",
61+
type = Unresolved(".com.example.Hello"),
62+
number = 1
63+
)
64+
val helloMsg = UserDefined.Message(
65+
owner = package1,
66+
name = "Hello",
67+
fields = listOf(
68+
worldField,
69+
ProtoField(
70+
name = "my_int",
71+
type = Primitive.INT32,
72+
number = 2,
73+
repeated = true
74+
)
75+
)
76+
)
77+
val worldMsg = UserDefined.Message(
78+
owner = Owner.MsgRef(helloMsg),
79+
name = "World",
80+
fields = listOf(
81+
helloField,
82+
ProtoField(
83+
name = "my_long",
84+
type = Primitive.INT64,
85+
number = 2
86+
)
87+
)
88+
)
89+
worldField.type = worldMsg
90+
helloField.type = helloMsg
91+
92+
assertThat(helloMsg.fields[0].toString()).contains(".com.example.Hello.World")
93+
}
94+
}

0 commit comments

Comments
 (0)