From d47e48b7b7f01f84e1f6759931f7ad870fb90ef4 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 6 Oct 2023 09:21:48 -0400 Subject: [PATCH 001/573] firebase-dataconnect skeleton added --- firebase-dataconnect/CHANGELOG.md | 1 + firebase-dataconnect/README.md | 116 ++++++++++ firebase-dataconnect/api.txt | 8 + .../firebase-dataconnect.gradle | 156 +++++++++++++ firebase-dataconnect/gradle.properties | 2 + firebase-dataconnect/lint.xml | 8 + firebase-dataconnect/proguard.txt | 18 ++ firebase-dataconnect/scripts/README.md | 13 ++ .../scripts/postgres_dbinit.sh | 10 + firebase-dataconnect/scripts/servers.json | 22 ++ firebase-dataconnect/scripts/start.sh | 40 ++++ .../src/androidTest/AndroidManifest.xml | 13 ++ .../dataconnect/FirebaseDataConnectTest.kt | 56 +++++ .../src/main/AndroidManifest.xml | 17 ++ .../dataconnect/FirebaseDataConnect.kt | 82 +++++++ .../com/google/firebase/dataconnect/Logger.kt | 54 +++++ .../firematdata/v0/data_service.proto | 205 ++++++++++++++++++ .../firematdata/v0/data_service_stream.proto | 42 ++++ .../src/test/AndroidManifest.xml | 13 ++ .../dataconnect/FirebaseDataConnectTest.kt | 28 +++ subprojects.cfg | 1 + 21 files changed, 905 insertions(+) create mode 100644 firebase-dataconnect/CHANGELOG.md create mode 100644 firebase-dataconnect/README.md create mode 100644 firebase-dataconnect/api.txt create mode 100644 firebase-dataconnect/firebase-dataconnect.gradle create mode 100644 firebase-dataconnect/gradle.properties create mode 100644 firebase-dataconnect/lint.xml create mode 100644 firebase-dataconnect/proguard.txt create mode 100644 firebase-dataconnect/scripts/README.md create mode 100755 firebase-dataconnect/scripts/postgres_dbinit.sh create mode 100644 firebase-dataconnect/scripts/servers.json create mode 100755 firebase-dataconnect/scripts/start.sh create mode 100644 firebase-dataconnect/src/androidTest/AndroidManifest.xml create mode 100644 firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt create mode 100644 firebase-dataconnect/src/main/AndroidManifest.xml create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt create mode 100644 firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service.proto create mode 100644 firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto create mode 100644 firebase-dataconnect/src/test/AndroidManifest.xml create mode 100644 firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md new file mode 100644 index 00000000000..79e701b844d --- /dev/null +++ b/firebase-dataconnect/CHANGELOG.md @@ -0,0 +1 @@ +# Unreleased diff --git a/firebase-dataconnect/README.md b/firebase-dataconnect/README.md new file mode 100644 index 00000000000..60cdc8eae10 --- /dev/null +++ b/firebase-dataconnect/README.md @@ -0,0 +1,116 @@ +# firebase-dataconnect + +This is the Firebase Data Connect component of the Firebase Android SDK. + +## Building + +All Gradle commands should be run from the source root (which is one level up +from this folder). See the README.md in the source root for instructions on +publishing/testing Firebase Data Connect. + +To build Firebase Data Connect, from the source root run: +```bash +./gradlew :firebase-dataconnect:assembleRelease +``` + +## Unit Testing + +To run unit tests for Firebase Data Connect, from the source root run: +```bash +./gradlew :firebase-dataconnect:check +``` + +## Integration Testing + +Running integration tests requires a Firebase project because they would try +to connect to the Firebase Data Connect backends. + +See [here](../README.md#project-setup) for how to setup a project. + +Once you setup the project, download `google-services.json` and place it in +the source root. + +Make sure you have created a Firebase Data Connect instance for your project, +before you proceed. + +By default, integration tests run against the Firebase Data Connect emulator. + +### Setting up the Firebase Data Connect Emulator + +The integration tests require that the Firebase Data Connect emulator is running +on port NNNN (TODO: fill in correct value), which is default when running it via +CLI. + + * [Install the Firebase CLI](https://firebase.google.com/docs/cli/). + ``` + npm install -g firebase-tools + ``` + * [Install the Firebase Data Connect + emulator](https://firebase.google.com/docs/FIX_URL/security/test-rules-emulator#install_the_emulator). + ``` + firebase setup:emulators:dataconnect + ``` + * Run the emulator + ``` + firebase emulators:start --only dataconnect + ``` + * Select the `Firebase Data Connect Integration Tests (Firebase Data Connect + Emulator)` run configuration to run all integration tests. + +To run the integration tests against prod, select +`DataConnectProdIntegrationTest` run configuration. + +### Run on Local Android Emulator + +Then simply run: +```bash +./gradlew :firebase-dataconnect:connectedCheck +``` + +### Run on Firebase Test Lab + +You can also test on Firebase Test Lab, which allow you to run the integration +tests on devices hosted in Google data center. + +See [here](../README.md#running-integration-tests-on-firebase-test-lab) for +instructions of how to setup Firebase Test Lab for your project. + +Run: +```bash +./gradlew :firebase-dataconnect:deviceCheck +``` + +## Code Formatting + +Run below to format Kotlin code: +```bash +./gradlew :firebase-dataconnect:ktfmtFormat +``` + +Run below to format Java code: +```bash +./gradlew :firebase-dataconnect:googleJavaFormat +``` + +See [here](../README.md#code-formatting) if you want to be able to format code +from within Android Studio. + +## Build Local Jar of Firebase Data Connect SDK + +```bash +./gradlew -PprojectsToPublish="firebase-dataconnect" publishReleasingLibrariesToMavenLocal +``` + +Developers may then take a dependency on these locally published versions by adding +the `mavenLocal()` repository to your [repositories +block](https://docs.gradle.org/current/userguide/declaring_repositories.html) in +your app module's build.gradle. + +## Misc +After importing the project into Android Studio and building successfully +for the first time, Android Studio will delete the run configuration xml files +in `./idea/runConfigurations`. Undo these changes with the command: + +``` +$ git checkout .idea/runConfigurations +``` diff --git a/firebase-dataconnect/api.txt b/firebase-dataconnect/api.txt new file mode 100644 index 00000000000..c9f0db4470f --- /dev/null +++ b/firebase-dataconnect/api.txt @@ -0,0 +1,8 @@ +// Signature format: 2.0 +package com.google.firebase.dataconnect { + + public class FirebaseDataConnect { + } + +} + diff --git a/firebase-dataconnect/firebase-dataconnect.gradle b/firebase-dataconnect/firebase-dataconnect.gradle new file mode 100644 index 00000000000..eaab7cefcec --- /dev/null +++ b/firebase-dataconnect/firebase-dataconnect.gradle @@ -0,0 +1,156 @@ +// Copyright 2023 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. + +plugins { + id 'firebase-library' + id("kotlin-android") + id 'com.google.protobuf' +} + +firebaseLibrary { + libraryGroup "dataconnect" + publishSources = true + testLab { + enabled = true + timeout = '30m' + } +} + +protobuf { + // Configure the protoc executable + protoc { + // Download from repositories + artifact = "com.google.protobuf:protoc:$protocVersion" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" + } + } + generateProtoTasks { + all().each { task -> + task.builtins { + java { option 'lite' } + } + task.plugins { + grpc { + option 'lite' + } + } + } + } +} + +android { + adbOptions { + timeOutInMs 60 * 1000 + } + + namespace "com.google.firebase.dataconnect" + compileSdkVersion project.targetSdkVersion + defaultConfig { + targetSdkVersion project.targetSdkVersion + minSdkVersion 21 + versionName version + multiDexEnabled true + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard.txt' + } + + sourceSets { + main { + proto { + srcDir 'src/proto' + } + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + testOptions.unitTests.includeAndroidResources = true + +} + +tasks.withType(Test) { + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 +} + +googleJavaFormat { +} + +dependencies { + androidTestAnnotationProcessor 'com.google.auto.value:auto-value:1.6.5' + androidTestImplementation "androidx.annotation:annotation:1.1.0" + androidTestImplementation "androidx.test.ext:junit:$androidxTestJUnitVersion" + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + androidTestImplementation 'junit:junit:4.13.2' + androidTestImplementation 'org.mockito:mockito-android:2.25.0' + androidTestImplementation 'org.mockito:mockito-core:2.25.0' + androidTestImplementation("com.google.truth:truth:$googleTruthVersion") { + exclude group: "org.codehaus.mojo", module: "animal-sniffer-annotations" + } + annotationProcessor 'com.google.auto.value:auto-value:1.6.5' + compileOnly 'com.google.auto.value:auto-value-annotations:1.6.6' + compileOnly 'javax.annotation:jsr250-api:1.0' + implementation "io.grpc:grpc-android:$grpcVersion" + implementation "io.grpc:grpc-okhttp:$grpcVersion" + implementation "io.grpc:grpc-protobuf-lite:$grpcVersion" + implementation "io.grpc:grpc-stub:$grpcVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" + implementation 'androidx.annotation:annotation:1.1.0' + implementation 'com.google.android.gms:play-services-base:18.0.1' + implementation 'com.google.android.gms:play-services-basement:18.1.0' + implementation 'com.google.android.gms:play-services-tasks:18.0.1' + implementation 'com.google.firebase:firebase-annotations:16.2.0' + implementation 'com.google.firebase:firebase-appcheck-interop:17.0.0' + implementation 'com.google.firebase:firebase-database-collection:18.0.1' + implementation project(':protolite-well-known-types') + implementation('com.google.firebase:firebase-auth-interop:19.0.2') { + exclude group: "com.google.firebase", module: "firebase-common" + } + implementation(project(":firebase-common")) + implementation(project(":firebase-common:ktx")) + implementation(project(":firebase-components")) + javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' + testCompileOnly "com.google.protobuf:protobuf-java:$protocVersion" + testImplementation "androidx.test:core:$androidxTestCoreVersion" + testImplementation "com.google.truth:truth:$googleTruthVersion" + testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0" + testImplementation "org.robolectric:robolectric:$robolectricVersion" + testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + testImplementation 'com.google.android.gms:play-services-tasks:18.0.1' + testImplementation 'com.google.guava:guava-testlib:12.0-rc2' + testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:2.25.0' + testImplementation project(':firebase-database-collection') + testImplementation project(':firebase-dataconnect') +} + +gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked" << "-Werror" + } +} + +// ========================================================================== +// Copy from here down if you want to use the google-services plugin in your +// androidTest integration tests. +// ========================================================================== +ext.packageName = "com.google.firebase.dataconnect" +apply from: '../gradle/googleServices.gradle' diff --git a/firebase-dataconnect/gradle.properties b/firebase-dataconnect/gradle.properties new file mode 100644 index 00000000000..2051ae7a898 --- /dev/null +++ b/firebase-dataconnect/gradle.properties @@ -0,0 +1,2 @@ +version=24.9.0 +latestReleasedVersion=24.8.1 diff --git a/firebase-dataconnect/lint.xml b/firebase-dataconnect/lint.xml new file mode 100644 index 00000000000..eb97348e7a1 --- /dev/null +++ b/firebase-dataconnect/lint.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/firebase-dataconnect/proguard.txt b/firebase-dataconnect/proguard.txt new file mode 100644 index 00000000000..b76f1281567 --- /dev/null +++ b/firebase-dataconnect/proguard.txt @@ -0,0 +1,18 @@ +# Needed for DNS resolution. Present in OpenJDK, but not Android +-dontwarn javax.naming.** + +# Don't warn about checkerframework +# +# Guava uses the checkerframework and the annotations +# can safely be ignored at runtime. +-dontwarn org.checkerframework.** + +# Guava warnings: +-dontwarn java.lang.ClassValue +-dontwarn com.google.j2objc.annotations.Weak +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement +-dontwarn javax.lang.model.element.Modifier + +# Okhttp warnings. +-dontwarn okio.** +-dontwarn com.google.j2objc.annotations.** diff --git a/firebase-dataconnect/scripts/README.md b/firebase-dataconnect/scripts/README.md new file mode 100644 index 00000000000..cf58d17c543 --- /dev/null +++ b/firebase-dataconnect/scripts/README.md @@ -0,0 +1,13 @@ +FireMAT PostgreSQL Emulator Helper + +This directory hosts some scripts useful for setting up a PostgreSQL server +and corresponding pgAdmin web UI in containers. By using containers, the +database is completely isolated from your local host and can be easily +reset. The idea is that the FireMAT emulator, which will eventually need to +talk to a _real_ PostgreSQL database, can launch these containers rather than +requiring PostgreSQL to be installed on your local machine. + +Rather than using Docker, which generally requires root privileges, the scripts +in this directory use `podman` (https://podman.io/), which natively supports +rootless operation. Googlers see go/dont-install-docker and go/rootless-podman. + diff --git a/firebase-dataconnect/scripts/postgres_dbinit.sh b/firebase-dataconnect/scripts/postgres_dbinit.sh new file mode 100755 index 00000000000..9f6a9b635ea --- /dev/null +++ b/firebase-dataconnect/scripts/postgres_dbinit.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -xev + +echo "POSTGRES_USER=$POSTGRES_USER" + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE DATABASE emulator; + GRANT ALL PRIVILEGES ON DATABASE emulator TO $POSTGRES_USER; +EOSQL diff --git a/firebase-dataconnect/scripts/servers.json b/firebase-dataconnect/scripts/servers.json new file mode 100644 index 00000000000..a4677676d2a --- /dev/null +++ b/firebase-dataconnect/scripts/servers.json @@ -0,0 +1,22 @@ +{ + "Servers": { + "1": { + "Name": "localhost", + "Group": "Servers", + "Host": "localhost", + "Port": 5432, + "MaintenanceDB": "postgres", + "Username": "postgres", + "UseSSHTunnel": 0, + "TunnelPort": "22", + "TunnelAuthentication": 0, + "KerberosAuthentication": false, + "ConnectionParameters": { + "sslmode": "prefer", + "connect_timeout": 10, + "sslcert": "/.postgresql/postgresql.crt", + "sslkey": "/.postgresql/postgresql.key" + } + } + } +} \ No newline at end of file diff --git a/firebase-dataconnect/scripts/start.sh b/firebase-dataconnect/scripts/start.sh new file mode 100755 index 00000000000..6f4e5686463 --- /dev/null +++ b/firebase-dataconnect/scripts/start.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +set -euo pipefail +set -xv + +# Create the podman "pod" if it is not already created. +# Bind the PostgreSQL server to port 5432 on the host, so that the host can connect to it. +# Bind the pgadmin server to port 8888 on the host, so that the host can connect to it. +# +if ! podman pod exists firemat_postgres_pod ; then + podman pod create -p 5432:5432 -p 8888:80 firemat_postgres_pod +fi + +# Start the PostgreSQL server. +podman \ + run \ + -dt \ + --rm \ + --pod firemat_postgres_pod \ + -e POSTGRES_HOST_AUTH_METHOD=trust \ + --mount "type=bind,ro,src=$(dirname "$0")/postgres_dbinit.sh,dst=/docker-entrypoint-initdb.d/postgres_dbinit.sh" \ + docker.io/library/postgres:15 + +# Start the pgadmin4 server. +podman \ + run \ + -dt \ + --rm \ + --pod firemat_postgres_pod \ + -e PGADMIN_DEFAULT_EMAIL=admin@google.com \ + -e PGADMIN_DEFAULT_PASSWORD=password \ + --mount "type=bind,ro,src=$(dirname "$0")/servers.json,dst=/pgadmin4/servers.json" \ + docker.io/dpage/pgadmin4 + +set +xv + +echo +echo "PostegreSQL server running on port 5432" +echo "pgAdmin web server running on port 8888, which can be viewed by browsing to http://localhost:8888" +echo "To shut everything down, run: podman pod stop firemat_postgres_pod" diff --git a/firebase-dataconnect/src/androidTest/AndroidManifest.xml b/firebase-dataconnect/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000000..c1d48258132 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt new file mode 100644 index 00000000000..2cc325c871d --- /dev/null +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -0,0 +1,56 @@ +// Copyright 2023 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.dataconnect + +import android.util.Log +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import google.internal.firebase.firemat.v0.DataServiceGrpc +import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteQueryRequest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock + +@RunWith(AndroidJUnit4::class) +class FirebaseDataConnectTest { + + @Test + fun helloWorld() { + val logger = mock(Logger::class.java) + val managedChannel = + createManagedChannel( + getApplicationContext(), + "10.0.2.2:3628", + GrpcConnectionEncryption.PLAINTEXT, + logger + ) + + val stub = DataServiceGrpc.newBlockingStub(managedChannel) + val projectId = "ZzyzxTestProject" + val location = "ZzyzxTestLocation" + + val request = + ExecuteQueryRequest.newBuilder().run { + name = + "projects/${projectId}/locations/${location}/services/s/operationSets/crud/revisions/r" + operationName = "listPosts" + build() + } + + Log.w("zzyzx", "Sending request: ${request}") + val response = stub.executeQuery(request) + Log.w("zzyzx", "Got response: ${response}") + } +} diff --git a/firebase-dataconnect/src/main/AndroidManifest.xml b/firebase-dataconnect/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..9bc28f3e58b --- /dev/null +++ b/firebase-dataconnect/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt new file mode 100644 index 00000000000..01d50f8c174 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -0,0 +1,82 @@ +// Copyright 2023 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.dataconnect + +import android.content.Context +import com.google.android.gms.security.ProviderInstaller +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import io.grpc.android.AndroidChannelBuilder +import java.util.concurrent.TimeUnit + +enum class GrpcConnectionEncryption { + PLAINTEXT, + ENCRYPTED, +} + +/** + * Open a GRPC connection. + * + * @param context A context to use; this context will simply be used to get the application context; + * therefore, specifying a transient context, such as an `Activity`, will _not_ result in that + * context being leaked. + * @param host The host name of the server to which to connect. (e.g. `"firestore.googleapis.com"`, + * `"10.0.2.2:3628"`) + * @param encryption The encryption to use. + * @param logger A logger to use. + */ +fun createManagedChannel( + context: Context, + host: String, + encryption: GrpcConnectionEncryption, + logger: Logger +): ManagedChannel { + upgradeAndroidSecurityProvider(context, logger) + + val channelBuilder = ManagedChannelBuilder.forTarget(host) + + when (encryption) { + GrpcConnectionEncryption.PLAINTEXT -> channelBuilder.usePlaintext() + GrpcConnectionEncryption.ENCRYPTED -> {} + } + + // Ensure gRPC recovers from a dead connection. This is not typically necessary, as + // the OS will usually notify gRPC when a connection dies. But not always. This acts as a + // failsafe. + channelBuilder.keepAliveTime(30, TimeUnit.SECONDS) + + // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel to + // respond more gracefully to network change events, such as switching from cellular to wifi. + return AndroidChannelBuilder.usingBuilder(channelBuilder) + .context(context.applicationContext) + .build() +} + +/** + * Upgrade the Android security provider using Google Play Services. + * + * We need to upgrade the Security Provider before any network channels are initialized because + * okhttp maintains a list of supported providers that is initialized when the JVM first resolves + * the static dependencies of ManagedChannel. + * + * If initialization fails for any reason, then a warning is logged and this function returns as if + * successful. + */ +private fun upgradeAndroidSecurityProvider(context: Context, logger: Logger) { + try { + ProviderInstaller.installIfNeeded(context.applicationContext) + } catch (e: Exception) { + logger.warn(e) { "Failed to update ssl context" } + } +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt new file mode 100644 index 00000000000..510cb4b2385 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt @@ -0,0 +1,54 @@ +// Copyright 2023 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.dataconnect + +import android.util.Log + +interface Logger { + + val name: String + var level: Level + + fun info(message: () -> Any?) + fun debug(message: () -> Any?) + fun warn(message: () -> Any?) + fun warn(e: Throwable?, message: () -> Any?) + + enum class Level { + DEBUG, + INFO, + WARNING, + } +} + +class LoggerImpl(override val name: String, override var level: Logger.Level) : Logger { + + override fun info(message: () -> Any?) { + if (level == Logger.Level.INFO || level == Logger.Level.DEBUG) { + Log.i("FirebaseDataConnect", message().toString()) + } + } + + override fun debug(message: () -> Any?) { + if (level == Logger.Level.DEBUG) { + Log.d("FirebaseDataConnect", message().toString()) + } + } + + override fun warn(message: () -> Any?) = warn(null, message) + + override fun warn(e: Throwable?, message: () -> Any?) { + Log.w("FirebaseDataConnect", message().toString(), e) + } +} diff --git a/firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service.proto b/firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service.proto new file mode 100644 index 00000000000..e5c741bad80 --- /dev/null +++ b/firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service.proto @@ -0,0 +1,205 @@ +// Adapted from http://google3/google/internal/firebase/firematdata/v0/data_service.proto;rcl=568574236 + +// API protos for the FireMAT Private API DataService. + +syntax = "proto3"; + +package google.internal.firebase.firemat.v0; + +import "google/api/annotations.proto"; +import "google/protobuf/struct.proto"; + +service DataService { + // REST API for executing a single pre-defined query. + // Use `operationSets/*/revisions/latest` to reference the most recent + // revision. + rpc ExecuteQuery(ExecuteQueryRequest) returns (ExecuteQueryResponse) { + option (google.api.http) = { + post: "/v0/{name=projects/*/locations/*/services/*/operationSets/*/revisions/*}:executeQuery" + body: "*" + }; + } + + // REST API for executing a single pre-defined mutation. + // Use `operationSets/*/revisions/latest` to reference the most recent + // revision. + rpc ExecuteMutation(ExecuteMutationRequest) + returns (ExecuteMutationResponse) { + option (google.api.http) = { + post: "/v0/{name=projects/*/locations/*/services/*/operationSets/*/revisions/*}:executeMutation" + body: "*" + }; + } + + // Admin access for executing GQL queries and mutations. + rpc ExecuteGraphql(GraphqlRequest) returns (GraphqlResult) { + option (google.api.http) = { + post: "/v0/{name=projects/*/locations/*/services/*}:executeGraphql" + body: "*" + }; + } + + // Admin access for executing GQL queries. + rpc ExecuteGraphqlRead(GraphqlRequest) returns (GraphqlResult) { + option (google.api.http) = { + post: "/v0/{name=projects/*/locations/*/services/*}:executeGraphqlRead" + body: "*" + }; + } +} + +message ExecuteQueryRequest { + // The resource name of the operation set revision that contains the query to + // execute, in the format: + // ``` + // projects/{project}/locations/{location}/services/{service}/operationSets/{operation_set}/revisions/{revision} + // ``` + // This is left empty when it is within StreamRequest. + string name = 1; + + // The name of the GraphQL operation. See + // https://graphql.org/learn/queries/#operation-name for more context. + string operation_name = 2; + + // Arbitrary JSON parameters (GraphQL variables) for the operation. + google.protobuf.Struct variables = 3; + + // The raw SQL to execute, along with necessary configuration for connecting + // to the Cloud SQL instance. This option is only intended for the private + // API, for testing purposes, and will be removed for launch. If this is + // specified, the resource name in the field `revision` is ignored. + SqlStatement sql_statement = 4; +} + +message ExecuteMutationRequest { + // The resource name of the operation set revision that contains the mutation + // to execute, in the format: + // ``` + // projects/{project}/locations/{location}/services/{service}/operationSets/{operation_set}/revisions/{revision} + // ``` + // This is left empty when it is within StreamRequest. + string name = 1; + + // The name of the GraphQL operation. See + // https://graphql.org/learn/queries/#operation-name for more context. + string operation_name = 2; + + // Arbitrary JSON parameters (GraphQL variables) for the operation. + google.protobuf.Struct variables = 3; + + // The raw SQL to execute, along with necessary configuration for connecting + // to the Cloud SQL instance. This option is only intended for the private + // API, for testing purposes, and will be removed for launch. If this is + // specified, the resource name in the field `revision` is ignored. + SqlStatement sql_statement = 4; +} + +message GraphqlRequest { + // The FireMAT instance name, in the format: + // ``` + // projects/{project}/locations/{location}/services/{service} + // ``` + string name = 1; + + // Values for GraphQL variables defined in the request. + google.protobuf.Struct variables = 3; + + // Auth token JSON for impersonation. Bypass `@auth` if left empty. + google.protobuf.Struct auth = 4; + + // The name of the GraphQL operation. See + // https://graphql.org/learn/queries/#operation-name for more context. + string operation_name = 5; + + // For an arbitrary query, specify `query`. See + // ​​https://graphql.org/learn/serving-over-http/. + // For a predefined query, specify `operation_set`. + oneof query_type { + string query = 2; + + // Scope to the surface storing pre-defined GQLs. + // for the latest revision + // /revisions/ otherwise + string operation_set = 6; + } +} + +message ExecuteQueryResponse { + google.protobuf.Struct data = 1; + repeated GraphqlError errors = 2; +} + +message ExecuteMutationResponse { + google.protobuf.Struct data = 1; + repeated GraphqlError errors = 2; +} + +message GraphqlResult { + google.protobuf.Struct data = 1; + repeated GraphqlError errors = 2; +} + +// Similar to cs/google3/java/com/google/cloud/boq/graphql/graphql.proto +// See https://spec.graphql.org/October2021/#sec-Errors +message GraphqlError { + // Description of the error, intended for the developer. + string message = 1; + + // The location in the query where the error occurred. + repeated SourceLocation locations = 2; + + // The result field which could not be populated due to error. + google.protobuf.ListValue path = 3; + + // Additional error information in error extensions. + GraphqlErrorExtensions extensions = 6; +} + +message SourceLocation { + int32 line = 1; + int32 column = 2; +} + +// An error that occurred while executing a GraphQL query. +message GraphqlErrorExtensions { + string file = 1; +} + +// The below three protos are intended ONLY for the private v0 API. For launch, +// configuration related to Cloud SQL connections will be stored and updated as +// part of the FireMAT control plane. + +// The SQL statement to be run against the specified Cloud SQL instance. +message SqlStatement { + CloudSqlConnection connection = 1; + string sql = 2; +} + +// Configuration information for connecting to the Cloud SQL instance. +message CloudSqlConnection { + enum DatabaseType { + DATABASE_TYPE_UNSPECIFIED = 0; + POSTGRES = 1; + MYSQL = 2; + } + DatabaseType type = 1; + + // The name of the Cloud SQL instance, in the format: + // ``` + // :: + // ``` + // For example: firebase-staging:us-central1:postgres + string instance = 2; + + // The name of the database. + string database = 3; + + // The credentials for accessing the Cloud SQL database. + CloudSqlCredential credentials = 4; +} + +// Credential info for the Cloud SQL instance. +message CloudSqlCredential { + string username = 1; + string password = 2; +} diff --git a/firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto b/firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto new file mode 100644 index 00000000000..1f4498bc566 --- /dev/null +++ b/firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto @@ -0,0 +1,42 @@ +// Adapted from http://google3/google/internal/firebase/firematdata/v0/data_service_stream.proto;rcl=553315079 + +// API protos for the FireMAT Private API DataService streaming API. + +syntax = "proto3"; + +package google.internal.firebase.firemat.v0; + +import "google/api/annotations.proto"; +import "google/internal/firebase/firematdata/v0/data_service.proto"; + +service DataServiceStreaming { + // Bi-directional streaming API used by client SDKs. + // Use `operationSets/*/revisions/latest` to reference the most recent + // revision. + rpc Stream(stream StreamRequest) returns (stream StreamResponse) { + option (google.api.http) = { + post: "/v0/{name=projects/*/locations/*/services/*/operationSets/*/revisions/*}:stream" + body: "*" + }; + } +} + +message StreamRequest { + // The resource name of the operation set revision to execute, in the format: + // ``` + // projects/{project}/locations/{location}/services/{service}/operationSets/{operation_set}/revisions/{revision} + // ``` + string name = 1; + + oneof operation { + ExecuteQueryRequest query = 2; + ExecuteMutationRequest mutation = 3; + } +} + +message StreamResponse { + oneof operation_response { + ExecuteQueryResponse query = 1; + ExecuteMutationResponse mutation = 2; + } +} diff --git a/firebase-dataconnect/src/test/AndroidManifest.xml b/firebase-dataconnect/src/test/AndroidManifest.xml new file mode 100644 index 00000000000..151c7fb817a --- /dev/null +++ b/firebase-dataconnect/src/test/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt new file mode 100644 index 00000000000..4501840384e --- /dev/null +++ b/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -0,0 +1,28 @@ +// Copyright 2023 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.dataconnect + +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class FirebaseDataConnectTest { + + @Test + fun `FirebaseDataConnectTest Hello World test`() { + System.out.println("FirebaseDataConnectTest Hello World test") + } +} diff --git a/subprojects.cfg b/subprojects.cfg index a86bc0b5e44..02ad5f232f5 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -26,6 +26,7 @@ firebase-crashlytics-ndk firebase-database firebase-database:ktx firebase-database-collection +firebase-dataconnect firebase-datatransport firebase-dynamic-links firebase-dynamic-links:ktx From 59050c6387b6c61f26c81a82a1eb69decc51ed0e Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 6 Oct 2023 10:06:43 -0400 Subject: [PATCH 002/573] fix firemat emulator port number: 3628 -> 9510 --- .../com/google/firebase/dataconnect/FirebaseDataConnectTest.kt | 2 +- .../java/com/google/firebase/dataconnect/FirebaseDataConnect.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 2cc325c871d..f6fc4b84b5d 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -32,7 +32,7 @@ class FirebaseDataConnectTest { val managedChannel = createManagedChannel( getApplicationContext(), - "10.0.2.2:3628", + "10.0.2.2:9510", GrpcConnectionEncryption.PLAINTEXT, logger ) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 01d50f8c174..147dc9bd2ed 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -32,7 +32,7 @@ enum class GrpcConnectionEncryption { * therefore, specifying a transient context, such as an `Activity`, will _not_ result in that * context being leaked. * @param host The host name of the server to which to connect. (e.g. `"firestore.googleapis.com"`, - * `"10.0.2.2:3628"`) + * `"10.0.2.2:9510"`) * @param encryption The encryption to use. * @param logger A logger to use. */ From 91697e69bd5721ba6b3bf401363bda8b6396c279 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 6 Oct 2023 12:30:37 -0400 Subject: [PATCH 003/573] FirebaseDataConnect instance and getInstance() added --- .../dataconnect/FirebaseDataConnectTest.kt | 55 ++++++++++++++++ .../dataconnect/FirebaseDataConnect.kt | 22 +++++++ .../FirebaseDataConnectRegistrar.kt | 63 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index f6fc4b84b5d..4c2a69448df 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -17,8 +17,14 @@ package com.google.firebase.dataconnect import android.util.Log import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.firebase.Firebase +import com.google.firebase.app +import com.google.firebase.initialize +import com.google.firebase.options import google.internal.firebase.firemat.v0.DataServiceGrpc import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteQueryRequest +import java.util.UUID import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock @@ -26,6 +32,48 @@ import org.mockito.Mockito.mock @RunWith(AndroidJUnit4::class) class FirebaseDataConnectTest { + @Test + fun instance_should_return_a_non_null_instance() { + assertThat(FirebaseDataConnect.instance).isNotNull() + } + + @Test + fun instance_should_always_return_the_same_instance() { + val instance1 = FirebaseDataConnect.instance + val instance2 = FirebaseDataConnect.instance + assertThat(instance1).isSameInstanceAs(instance2) + } + + @Test + fun getInstance_with_default_app_should_return_same_instance_as_the_instance_getter() { + val instanceFromGetInstance = FirebaseDataConnect.getInstance(Firebase.app) + assertThat(instanceFromGetInstance).isSameInstanceAs(FirebaseDataConnect.instance) + } + + @Test + fun getInstance_with_non_default_app_should_return_non_default_instance() { + val nonDefaultApp = createNonDefaultFirebaseApp() + val nonDefaultInstanceFromGetInstance = FirebaseDataConnect.getInstance(nonDefaultApp) + assertThat(nonDefaultInstanceFromGetInstance).isNotSameInstanceAs(FirebaseDataConnect.instance) + } + + @Test + fun getInstance_with_the_same_non_default_apps_should_return_the_same_instances() { + val nonDefaultApp = createNonDefaultFirebaseApp() + val nonDefaultInstance1 = FirebaseDataConnect.getInstance(nonDefaultApp) + val nonDefaultInstance2 = FirebaseDataConnect.getInstance(nonDefaultApp) + assertThat(nonDefaultInstance1).isSameInstanceAs(nonDefaultInstance2) + } + + @Test + fun getInstance_with_distinct_non_default_apps_should_return_distinct_instances() { + val nonDefaultApp1 = createNonDefaultFirebaseApp() + val nonDefaultApp2 = createNonDefaultFirebaseApp() + val nonDefaultInstance1 = FirebaseDataConnect.getInstance(nonDefaultApp1) + val nonDefaultInstance2 = FirebaseDataConnect.getInstance(nonDefaultApp2) + assertThat(nonDefaultInstance1).isNotSameInstanceAs(nonDefaultInstance2) + } + @Test fun helloWorld() { val logger = mock(Logger::class.java) @@ -54,3 +102,10 @@ class FirebaseDataConnectTest { Log.w("zzyzx", "Got response: ${response}") } } + +private fun createNonDefaultFirebaseApp() = + Firebase.initialize( + Firebase.app.applicationContext, + Firebase.app.options, + UUID.randomUUID().toString() + ) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 147dc9bd2ed..63019d63318 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -15,10 +15,32 @@ package com.google.firebase.dataconnect import android.content.Context import com.google.android.gms.security.ProviderInstaller +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.app import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder import java.util.concurrent.TimeUnit +import kotlinx.coroutines.CoroutineDispatcher + +class FirebaseDataConnect +internal constructor( + private val firebaseApp: FirebaseApp, + private val backgroundDispatcher: CoroutineDispatcher, + private val blockingDispatcher: CoroutineDispatcher, +) { + + companion object { + @JvmStatic + val instance: FirebaseDataConnect + get() = getInstance(Firebase.app) + + @JvmStatic + fun getInstance(app: FirebaseApp): FirebaseDataConnect = + app.get(FirebaseDataConnect::class.java) + } +} enum class GrpcConnectionEncryption { PLAINTEXT, diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt new file mode 100644 index 00000000000..b69183e555e --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt @@ -0,0 +1,63 @@ +// Copyright 2023 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.dataconnect + +import androidx.annotation.Keep +import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.components.Component +import com.google.firebase.components.ComponentRegistrar +import com.google.firebase.components.Dependency +import com.google.firebase.components.Qualified.qualified +import com.google.firebase.components.Qualified.unqualified +import com.google.firebase.platforminfo.LibraryVersionComponent +import kotlinx.coroutines.CoroutineDispatcher + +/** + * [ComponentRegistrar] for setting up [FirebaseDataConnect]. + * + * @hide + */ +@Keep +internal class FirebaseDataConnectRegistrar : ComponentRegistrar { + override fun getComponents() = + listOf( + Component.builder(FirebaseDataConnect::class.java) + .name(LIBRARY_NAME) + .add(Dependency.required(firebaseApp)) + .add(Dependency.required(backgroundDispatcher)) + .add(Dependency.required(blockingDispatcher)) + .factory { container -> + FirebaseDataConnect( + container.get(firebaseApp), + container.get(backgroundDispatcher), + container.get(blockingDispatcher), + ) + } + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME) + ) + + companion object { + private const val LIBRARY_NAME = "fire-dataconnect" + + private val firebaseApp = unqualified(FirebaseApp::class.java) + private val backgroundDispatcher = + qualified(Background::class.java, CoroutineDispatcher::class.java) + private val blockingDispatcher = + qualified(Blocking::class.java, CoroutineDispatcher::class.java) + } +} From f11a42f0a709c9a3f905a053e411070888eed02b Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 6 Oct 2023 16:38:52 -0400 Subject: [PATCH 004/573] FirebaseDataConnectTest.kt: exercise logic to create a post and query posts --- .../dataconnect/FirebaseDataConnectTest.kt | 60 +++++++++++++++---- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 4c2a69448df..a26a72b8f0f 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -22,7 +22,10 @@ import com.google.firebase.Firebase import com.google.firebase.app import com.google.firebase.initialize import com.google.firebase.options +import com.google.protobuf.Struct +import com.google.protobuf.Value import google.internal.firebase.firemat.v0.DataServiceGrpc +import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteMutationRequest import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteQueryRequest import java.util.UUID import org.junit.Test @@ -89,17 +92,52 @@ class FirebaseDataConnectTest { val projectId = "ZzyzxTestProject" val location = "ZzyzxTestLocation" - val request = - ExecuteQueryRequest.newBuilder().run { - name = - "projects/${projectId}/locations/${location}/services/s/operationSets/crud/revisions/r" - operationName = "listPosts" - build() - } - - Log.w("zzyzx", "Sending request: ${request}") - val response = stub.executeQuery(request) - Log.w("zzyzx", "Got response: ${response}") + run { + val request = + ExecuteMutationRequest.newBuilder().run { + name = + "projects/${projectId}/locations/${location}/services/s/operationSets/crud/revisions/r" + operationName = "createPost" + variables = + Struct.newBuilder().run { + putFields( + "data", + Value.newBuilder().run { + setStructValue( + Struct.newBuilder().run { + putFields( + "content", + Value.newBuilder().setStringValue("${System.currentTimeMillis()}").build() + ) + build() + } + ) + build() + } + ) + build() + } + build() + } + + Log.w("zzyzx", "Sending mutation request: ${request}") + val response = stub.executeMutation(request) + Log.w("zzyzx", "Got mutation response: ${response}") + } + + run { + val request = + ExecuteQueryRequest.newBuilder().run { + name = + "projects/${projectId}/locations/${location}/services/s/operationSets/crud/revisions/r" + operationName = "listPosts" + build() + } + + Log.w("zzyzx", "Sending query request: ${request}") + val response = stub.executeQuery(request) + Log.w("zzyzx", "Got query response: ${response}") + } } } From 2071ba62b16c1d95d994290bb4084b0764b599dc Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 6 Oct 2023 16:44:37 -0400 Subject: [PATCH 005/573] include id when creating a post --- .../google/firebase/dataconnect/FirebaseDataConnectTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index a26a72b8f0f..d1d41b64b9a 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -105,6 +105,10 @@ class FirebaseDataConnectTest { Value.newBuilder().run { setStructValue( Struct.newBuilder().run { + putFields( + "id", + Value.newBuilder().setStringValue(UUID.randomUUID().toString()).build() + ) putFields( "content", Value.newBuilder().setStringValue("${System.currentTimeMillis()}").build() From 0195904cbb7eac04573c5f2f76bd630cce70339c Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 13 Oct 2023 11:18:58 -0400 Subject: [PATCH 006/573] start.sh: save podman state into volumes (#501) --- firebase-dataconnect/scripts/start.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/firebase-dataconnect/scripts/start.sh b/firebase-dataconnect/scripts/start.sh index 6f4e5686463..f6d861d9d83 100755 --- a/firebase-dataconnect/scripts/start.sh +++ b/firebase-dataconnect/scripts/start.sh @@ -3,10 +3,12 @@ set -euo pipefail set -xv +# Determine the absolute path of the directory containing this file. +readonly SCRIPT_DIR="$(readlink -f $(dirname "$0"))" + # Create the podman "pod" if it is not already created. # Bind the PostgreSQL server to port 5432 on the host, so that the host can connect to it. # Bind the pgadmin server to port 8888 on the host, so that the host can connect to it. -# if ! podman pod exists firemat_postgres_pod ; then podman pod create -p 5432:5432 -p 8888:80 firemat_postgres_pod fi @@ -18,7 +20,8 @@ podman \ --rm \ --pod firemat_postgres_pod \ -e POSTGRES_HOST_AUTH_METHOD=trust \ - --mount "type=bind,ro,src=$(dirname "$0")/postgres_dbinit.sh,dst=/docker-entrypoint-initdb.d/postgres_dbinit.sh" \ + --mount "type=bind,ro,src=${SCRIPT_DIR}/postgres_dbinit.sh,dst=/docker-entrypoint-initdb.d/postgres_dbinit.sh" \ + --mount "type=volume,src=firemat_pgdata,dst=/var/lib/postgresql/data" \ docker.io/library/postgres:15 # Start the pgadmin4 server. @@ -29,7 +32,8 @@ podman \ --pod firemat_postgres_pod \ -e PGADMIN_DEFAULT_EMAIL=admin@google.com \ -e PGADMIN_DEFAULT_PASSWORD=password \ - --mount "type=bind,ro,src=$(dirname "$0")/servers.json,dst=/pgadmin4/servers.json" \ + --mount "type=bind,ro,src=${SCRIPT_DIR}/servers.json,dst=/pgadmin4/servers.json" \ + --mount "type=volume,src=firemat_pgadmin_data,dst=/var/lib/pgadmin" \ docker.io/dpage/pgadmin4 set +xv From 6da47fd12f24b60011aafc586b6556223509bd2d Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 13 Oct 2023 13:11:02 -0400 Subject: [PATCH 007/573] FirebaseDataConnectSettings.kt added --- .../dataconnect/FirebaseDataConnect.kt | 15 +++++ .../FirebaseDataConnectSettings.kt | 63 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 63019d63318..5c83edf37be 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -22,6 +22,9 @@ import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write import kotlinx.coroutines.CoroutineDispatcher class FirebaseDataConnect @@ -31,6 +34,18 @@ internal constructor( private val blockingDispatcher: CoroutineDispatcher, ) { + private val settingsLock = ReentrantReadWriteLock() + + var settings: FirebaseDataConnectSettings = FirebaseDataConnectSettings.defaultInstance + get() { + settingsLock.read { + return field + } + } + set(value) { + settingsLock.write { field = value } + } + companion object { @JvmStatic val instance: FirebaseDataConnect diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt new file mode 100644 index 00000000000..342d24a77ad --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt @@ -0,0 +1,63 @@ +// Copyright 2023 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.dataconnect + +class FirebaseDataConnectSettings private constructor(private val values: SettingsValues) { + + val hostName: String + get() = values.hostName + + val port: Int + get() = values.port + + val sslEnabled: Boolean + get() = values.sslEnabled + + companion object { + val defaultInstance + get() = + FirebaseDataConnectSettings( + SettingsValues(hostName = "firestore.googleapis.com", port = 443, sslEnabled = true) + ) + } + + fun withHostName(hostName: String) = FirebaseDataConnectSettings(values.copy(hostName = hostName)) + + fun withPort(port: Int) = FirebaseDataConnectSettings(values.copy(port = port)) + + fun withSslEnabled(sslEnabled: Boolean) = + FirebaseDataConnectSettings(values.copy(sslEnabled = sslEnabled)) + + fun withEmulatorValues() = this.withHostName("10.0.2.2").withPort(9510).withSslEnabled(false) + + override fun equals(other: Any?) = + when (other) { + this -> true + !is FirebaseDataConnectSettings -> false + else -> values.equals(other.values) + } + + override fun hashCode() = values.hashCode() + + override fun toString() = + "FirebaseDataConnectSettings{hostName=$hostName, port=$port, sslEnabled=$sslEnabled}" +} + +// Use a data class internally to store the settings to get the conveninence of the equals(), +// hashCode(), and copy() auto-generated methods. +private data class SettingsValues( + val hostName: String, + val port: Int, + val sslEnabled: Boolean, +) From 72d3b6c7490dbd1bc455e9173060622b9ce9c8bf Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 13 Oct 2023 16:33:31 -0400 Subject: [PATCH 008/573] FirebaseDataConnect.kt: added executeQuery() and executeMutation() --- .../dataconnect/FirebaseDataConnectTest.kt | 76 +------- .../dataconnect/FirebaseDataConnect.kt | 180 ++++++++++++------ .../FirebaseDataConnectSettings.kt | 37 +++- .../com/google/firebase/dataconnect/Logger.kt | 2 +- 4 files changed, 163 insertions(+), 132 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index d1d41b64b9a..a92520024c2 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -14,23 +14,15 @@ package com.google.firebase.dataconnect -import android.util.Log -import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.Firebase import com.google.firebase.app import com.google.firebase.initialize import com.google.firebase.options -import com.google.protobuf.Struct -import com.google.protobuf.Value -import google.internal.firebase.firemat.v0.DataServiceGrpc -import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteMutationRequest -import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteQueryRequest import java.util.UUID import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock @RunWith(AndroidJUnit4::class) class FirebaseDataConnectTest { @@ -79,69 +71,19 @@ class FirebaseDataConnectTest { @Test fun helloWorld() { - val logger = mock(Logger::class.java) - val managedChannel = - createManagedChannel( - getApplicationContext(), - "10.0.2.2:9510", - GrpcConnectionEncryption.PLAINTEXT, - logger - ) + val dc = FirebaseDataConnect.instance + dc.settings = dataConnectSettings { connectToEmulator() } - val stub = DataServiceGrpc.newBlockingStub(managedChannel) val projectId = "ZzyzxTestProject" val location = "ZzyzxTestLocation" - run { - val request = - ExecuteMutationRequest.newBuilder().run { - name = - "projects/${projectId}/locations/${location}/services/s/operationSets/crud/revisions/r" - operationName = "createPost" - variables = - Struct.newBuilder().run { - putFields( - "data", - Value.newBuilder().run { - setStructValue( - Struct.newBuilder().run { - putFields( - "id", - Value.newBuilder().setStringValue(UUID.randomUUID().toString()).build() - ) - putFields( - "content", - Value.newBuilder().setStringValue("${System.currentTimeMillis()}").build() - ) - build() - } - ) - build() - } - ) - build() - } - build() - } - - Log.w("zzyzx", "Sending mutation request: ${request}") - val response = stub.executeMutation(request) - Log.w("zzyzx", "Got mutation response: ${response}") - } - - run { - val request = - ExecuteQueryRequest.newBuilder().run { - name = - "projects/${projectId}/locations/${location}/services/s/operationSets/crud/revisions/r" - operationName = "listPosts" - build() - } - - Log.w("zzyzx", "Sending query request: ${request}") - val response = stub.executeQuery(request) - Log.w("zzyzx", "Got query response: ${response}") - } + dc.executeMutation( + projectId, + location, + "createPost", + mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") + ) + dc.executeQuery(projectId, location, "listPosts", emptyMap()) } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 5c83edf37be..ca785b8b681 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -13,11 +13,17 @@ // limitations under the License. package com.google.firebase.dataconnect -import android.content.Context import com.google.android.gms.security.ProviderInstaller import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app +import com.google.protobuf.NullValue +import com.google.protobuf.Struct +import com.google.protobuf.Value +import google.internal.firebase.firemat.v0.DataServiceGrpc +import google.internal.firebase.firemat.v0.DataServiceGrpc.DataServiceBlockingStub +import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteMutationRequest +import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteQueryRequest import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder @@ -33,8 +39,49 @@ internal constructor( private val backgroundDispatcher: CoroutineDispatcher, private val blockingDispatcher: CoroutineDispatcher, ) { + private val logger = LoggerImpl("FirebaseDataConnect", Logger.Level.DEBUG) private val settingsLock = ReentrantReadWriteLock() + private var settingsFrozen = false + + private val grpcChannel: ManagedChannel by lazy { + // Make sure that no further changes can be made to the settings. + settingsLock.write { settingsFrozen = true } + + // Upgrade the Android security provider using Google Play Services. + // + // We need to upgrade the Security Provider before any network channels are initialized because + // okhttp maintains a list of supported providers that is initialized when the JVM first + // resolves the static dependencies of ManagedChannel. + // + // If initialization fails for any reason, then a warning is logged and the original, + // un-upgraded security provider is used. + try { + ProviderInstaller.installIfNeeded(firebaseApp.applicationContext) + } catch (e: Exception) { + logger.warn(e) { "Failed to update ssl context" } + } + + val channelBuilder = ManagedChannelBuilder.forAddress(settings.hostName, settings.port) + if (!settings.sslEnabled) { + channelBuilder.usePlaintext() + } + + // Ensure gRPC recovers from a dead connection. This is not typically necessary, as + // the OS will usually notify gRPC when a connection dies. But not always. This acts as a + // failsafe. + channelBuilder.keepAliveTime(30, TimeUnit.SECONDS) + + // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel to + // respond more gracefully to network change events, such as switching from cellular to wifi. + AndroidChannelBuilder.usingBuilder(channelBuilder) + .context(firebaseApp.applicationContext) + .build() + } + + private val grpcStub: DataServiceBlockingStub by lazy { + DataServiceGrpc.newBlockingStub(grpcChannel) + } var settings: FirebaseDataConnectSettings = FirebaseDataConnectSettings.defaultInstance get() { @@ -43,9 +90,66 @@ internal constructor( } } set(value) { - settingsLock.write { field = value } + settingsLock.write { + if (settingsFrozen) { + throw IllegalStateException("settings cannot be modified after they are used") + } + field = value + } } + fun executeQuery( + projectId: String, + location: String, + operationName: String, + variables: Map + ): Struct { + val request = + ExecuteQueryRequest.newBuilder().let { + it.name = + "projects/${projectId}/locations/${location}/services/s/operationSets/crud/revisions/r" + it.operationName = operationName + it.variables = structFromMap(variables) + it.build() + } + + logger.debug { "executeQuery() sending request: $request" } + + val response = grpcStub.executeQuery(request) + + logger.debug { "executeQuery() got response: $response" } + + return response.data + } + + fun executeMutation( + projectId: String, + location: String, + operationName: String, + variables: Map + ): Struct { + val request = + ExecuteMutationRequest.newBuilder().let { + it.name = + "projects/${projectId}/locations/${location}/services/s/operationSets/crud/revisions/r" + it.operationName = operationName + it.variables = + Struct.newBuilder().run { + putFields("data", Value.newBuilder().setStructValue(structFromMap(variables)).build()) + build() + } + it.build() + } + + logger.debug { "executeMutation() sending request: $request" } + + val response = grpcStub.executeMutation(request) + + logger.debug { "executeMutation() got response: $response" } + + return response.data + } + companion object { @JvmStatic val instance: FirebaseDataConnect @@ -57,63 +161,21 @@ internal constructor( } } -enum class GrpcConnectionEncryption { - PLAINTEXT, - ENCRYPTED, -} - -/** - * Open a GRPC connection. - * - * @param context A context to use; this context will simply be used to get the application context; - * therefore, specifying a transient context, such as an `Activity`, will _not_ result in that - * context being leaked. - * @param host The host name of the server to which to connect. (e.g. `"firestore.googleapis.com"`, - * `"10.0.2.2:9510"`) - * @param encryption The encryption to use. - * @param logger A logger to use. - */ -fun createManagedChannel( - context: Context, - host: String, - encryption: GrpcConnectionEncryption, - logger: Logger -): ManagedChannel { - upgradeAndroidSecurityProvider(context, logger) - - val channelBuilder = ManagedChannelBuilder.forTarget(host) - - when (encryption) { - GrpcConnectionEncryption.PLAINTEXT -> channelBuilder.usePlaintext() - GrpcConnectionEncryption.ENCRYPTED -> {} +private fun structFromMap(map: Map): Struct = + Struct.newBuilder().run { + map.keys.sorted().forEach { key -> putFields(key, protoValueFromObject(map[key])) } + build() } - // Ensure gRPC recovers from a dead connection. This is not typically necessary, as - // the OS will usually notify gRPC when a connection dies. But not always. This acts as a - // failsafe. - channelBuilder.keepAliveTime(30, TimeUnit.SECONDS) - - // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel to - // respond more gracefully to network change events, such as switching from cellular to wifi. - return AndroidChannelBuilder.usingBuilder(channelBuilder) - .context(context.applicationContext) +private fun protoValueFromObject(obj: Any?): Value = + Value.newBuilder() + .run { + when (obj) { + null -> setNullValue(NullValue.NULL_VALUE) + is String -> setStringValue(obj) + is Boolean -> setBoolValue(obj) + is Double -> setNumberValue(obj) + else -> throw IllegalArgumentException("unsupported value type: ${obj::class}") + } + } .build() -} - -/** - * Upgrade the Android security provider using Google Play Services. - * - * We need to upgrade the Security Provider before any network channels are initialized because - * okhttp maintains a list of supported providers that is initialized when the JVM first resolves - * the static dependencies of ManagedChannel. - * - * If initialization fails for any reason, then a warning is logged and this function returns as if - * successful. - */ -private fun upgradeAndroidSecurityProvider(context: Context, logger: Logger) { - try { - ProviderInstaller.installIfNeeded(context.applicationContext) - } catch (e: Exception) { - logger.warn(e) { "Failed to update ssl context" } - } -} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt index 342d24a77ad..65d1da0c298 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt @@ -32,14 +32,36 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin ) } - fun withHostName(hostName: String) = FirebaseDataConnectSettings(values.copy(hostName = hostName)) + class Builder constructor(initialValues: FirebaseDataConnectSettings) { - fun withPort(port: Int) = FirebaseDataConnectSettings(values.copy(port = port)) + private var values = initialValues.values - fun withSslEnabled(sslEnabled: Boolean) = - FirebaseDataConnectSettings(values.copy(sslEnabled = sslEnabled)) + var hostName: String + get() = values.hostName + set(value) { + values = values.copy(hostName = value) + } - fun withEmulatorValues() = this.withHostName("10.0.2.2").withPort(9510).withSslEnabled(false) + var port: Int + get() = values.port + set(value) { + values = values.copy(port = value) + } + + var sslEnabled: Boolean + get() = values.sslEnabled + set(value) { + values = values.copy(sslEnabled = value) + } + + fun connectToEmulator() { + hostName = "10.0.2.2" + port = 9510 + sslEnabled = false + } + + fun build() = FirebaseDataConnectSettings(values) + } override fun equals(other: Any?) = when (other) { @@ -54,6 +76,11 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin "FirebaseDataConnectSettings{hostName=$hostName, port=$port, sslEnabled=$sslEnabled}" } +fun dataConnectSettings(block: FirebaseDataConnectSettings.Builder.() -> Unit) = + FirebaseDataConnectSettings.Builder(FirebaseDataConnectSettings.defaultInstance) + .apply(block) + .build() + // Use a data class internally to store the settings to get the conveninence of the equals(), // hashCode(), and copy() auto-generated methods. private data class SettingsValues( diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt index 510cb4b2385..135a8cb4f9d 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt @@ -32,7 +32,7 @@ interface Logger { } } -class LoggerImpl(override val name: String, override var level: Logger.Level) : Logger { +internal class LoggerImpl(override val name: String, override var level: Logger.Level) : Logger { override fun info(message: () -> Any?) { if (level == Logger.Level.INFO || level == Logger.Level.DEBUG) { From 2fdf72295d4308986344330979d10da9f80133ca Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 13 Oct 2023 22:49:30 -0400 Subject: [PATCH 009/573] FirebaseDataConnectSettings.kt: apply some cleanups recommended by Rodrigo --- .../FirebaseDataConnectSettings.kt | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt index 65d1da0c298..f988a8473ad 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt @@ -33,26 +33,9 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin } class Builder constructor(initialValues: FirebaseDataConnectSettings) { - - private var values = initialValues.values - - var hostName: String - get() = values.hostName - set(value) { - values = values.copy(hostName = value) - } - - var port: Int - get() = values.port - set(value) { - values = values.copy(port = value) - } - - var sslEnabled: Boolean - get() = values.sslEnabled - set(value) { - values = values.copy(sslEnabled = value) - } + var hostName = initialValues.hostName + var port = initialValues.port + var sslEnabled = initialValues.sslEnabled fun connectToEmulator() { hostName = "10.0.2.2" @@ -60,14 +43,17 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin sslEnabled = false } - fun build() = FirebaseDataConnectSettings(values) + fun build() = + FirebaseDataConnectSettings( + SettingsValues(hostName = hostName, port = port, sslEnabled = sslEnabled) + ) } override fun equals(other: Any?) = when (other) { this -> true !is FirebaseDataConnectSettings -> false - else -> values.equals(other.values) + else -> values == other.values } override fun hashCode() = values.hashCode() @@ -76,12 +62,12 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin "FirebaseDataConnectSettings{hostName=$hostName, port=$port, sslEnabled=$sslEnabled}" } -fun dataConnectSettings(block: FirebaseDataConnectSettings.Builder.() -> Unit) = +inline fun dataConnectSettings(block: FirebaseDataConnectSettings.Builder.() -> Unit) = FirebaseDataConnectSettings.Builder(FirebaseDataConnectSettings.defaultInstance) .apply(block) .build() -// Use a data class internally to store the settings to get the conveninence of the equals(), +// Use a data class internally to store the settings to get the convenience of the equals(), // hashCode(), and copy() auto-generated methods. private data class SettingsValues( val hostName: String, From d7a09338ee211d2f5506cef40496126bccdbf006 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 16 Oct 2023 15:55:55 -0400 Subject: [PATCH 010/573] google-services.json added with stub values so that it can be used out of the box with the firesql emulator --- firebase-dataconnect/google-services.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 firebase-dataconnect/google-services.json diff --git a/firebase-dataconnect/google-services.json b/firebase-dataconnect/google-services.json new file mode 100644 index 00000000000..b00b1a2d896 --- /dev/null +++ b/firebase-dataconnect/google-services.json @@ -0,0 +1,18 @@ +{ + "project_info": { + "project_number": "12345678901", + "firebase_url": "https://dataconnect-demo.firebaseio.com", + "project_id": "dataconnect-demo", + "storage_bucket": "dataconnect-demo.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:12345678901:android:1234567890abcdef123456", + "android_client_info": { + "package_name": "com.google.firebase.dataconnect" + } + } + ], + "configuration_version": "1" +} From fd5f3783a2bd674ece838e30bb51cd9f35226dff Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 16 Oct 2023 15:57:19 -0400 Subject: [PATCH 011/573] FirebaseDataConnectSettingsTest.kt added --- .../FirebaseDataConnectSettings.kt | 19 +- .../FirebaseDataConnectSettingsTest.kt | 315 ++++++++++++++++++ 2 files changed, 326 insertions(+), 8 deletions(-) create mode 100644 firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt index f988a8473ad..4882bf02c6f 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt @@ -24,6 +24,9 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin val sslEnabled: Boolean get() = values.sslEnabled + val builder: Builder + get() = Builder(this) + companion object { val defaultInstance get() = @@ -32,7 +35,7 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin ) } - class Builder constructor(initialValues: FirebaseDataConnectSettings) { + class Builder internal constructor(initialValues: FirebaseDataConnectSettings) { var hostName = initialValues.hostName var port = initialValues.port var sslEnabled = initialValues.sslEnabled @@ -50,10 +53,12 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin } override fun equals(other: Any?) = - when (other) { - this -> true - !is FirebaseDataConnectSettings -> false - else -> values == other.values + if (other === this) { + true + } else if (other !is FirebaseDataConnectSettings) { + false + } else { + values == other.values } override fun hashCode() = values.hashCode() @@ -63,9 +68,7 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin } inline fun dataConnectSettings(block: FirebaseDataConnectSettings.Builder.() -> Unit) = - FirebaseDataConnectSettings.Builder(FirebaseDataConnectSettings.defaultInstance) - .apply(block) - .build() + FirebaseDataConnectSettings.defaultInstance.builder.apply(block).build() // Use a data class internally to store the settings to get the convenience of the equals(), // hashCode(), and copy() auto-generated methods. diff --git a/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt b/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt new file mode 100644 index 00000000000..f1b10e2c32d --- /dev/null +++ b/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt @@ -0,0 +1,315 @@ +// Copyright 2023 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.dataconnect + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class FirebaseDataConnectSettingsTest { + + @Test + fun `defaultInstance properties should have the expected values`() { + FirebaseDataConnectSettings.defaultInstance.apply { + assertThat(hostName).isEqualTo("firestore.googleapis.com") + assertThat(port).isEqualTo(443) + assertThat(sslEnabled).isEqualTo(true) + } + } + + @Test + fun `builder should be initialized with values from the object given to the constructor`() { + val settings = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = 987654321 + sslEnabled = false + build() + } + settings.builder.apply { + assertThat(hostName).isEqualTo("abc123") + assertThat(port).isEqualTo(987654321) + assertThat(sslEnabled).isEqualTo(false) + } + } + + @Test + fun `builder can change the hostName`() { + val defaultSettings = FirebaseDataConnectSettings.defaultInstance + val settings = + defaultSettings.builder.run { + hostName = "abc123" + build() + } + settings.apply { + assertThat(hostName).isEqualTo("abc123") + assertThat(port).isEqualTo(defaultSettings.port) + assertThat(sslEnabled).isEqualTo(defaultSettings.sslEnabled) + } + } + + @Test + fun `builder can change the port`() { + val defaultSettings = FirebaseDataConnectSettings.defaultInstance + val settings = + defaultSettings.builder.run { + port = 987654321 + build() + } + settings.apply { + assertThat(hostName).isEqualTo(defaultSettings.hostName) + assertThat(port).isEqualTo(987654321) + assertThat(sslEnabled).isEqualTo(defaultSettings.sslEnabled) + } + } + + @Test + fun `builder can change the sslEnabled`() { + val defaultSettings = FirebaseDataConnectSettings.defaultInstance + val settings = + defaultSettings.builder.run { + sslEnabled = !defaultSettings.sslEnabled + build() + } + settings.apply { + assertThat(hostName).isEqualTo(defaultSettings.hostName) + assertThat(port).isEqualTo(defaultSettings.port) + assertThat(sslEnabled).isEqualTo(!defaultSettings.sslEnabled) + } + } + + @Test + fun `connectToEmulator() should set the correct values`() { + FirebaseDataConnectSettings.defaultInstance.builder.apply { + connectToEmulator() + assertThat(hostName).isEqualTo("10.0.2.2") + assertThat(port).isEqualTo(9510) + assertThat(sslEnabled).isEqualTo(false) + } + } + + @Test + @Suppress("ReplaceCallWithBinaryOperator") + fun `equals() should return true for same instance`() { + val settings = FirebaseDataConnectSettings.defaultInstance + assertThat(settings.equals(settings)).isTrue() + } + + @Test + fun `equals() should return false for null`() { + val settings = FirebaseDataConnectSettings.defaultInstance + assertThat(settings.equals(null)).isFalse() + } + + @Test + fun `equals() should return false for a different type`() { + val settings = FirebaseDataConnectSettings.defaultInstance + assertThat(settings.equals("an instance of a different class")).isFalse() + } + + @Test + @Suppress("ReplaceCallWithBinaryOperator") + fun `equals() should return true for distinct instances with the same property values`() { + val settings1 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = 987654321 + sslEnabled = true + build() + } + val settings2 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = 987654321 + sslEnabled = true + build() + } + // validate the assumption that `settings1` and `settings2` are distinct objects. + assertThat(settings1).isNotSameInstanceAs(settings2) + assertThat(settings1.equals(settings2)).isTrue() + } + + @Test + @Suppress("ReplaceCallWithBinaryOperator") + fun `equals() should return false when hostName differs`() { + val settings1 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = 987654321 + sslEnabled = true + build() + } + val settings2 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "zzzzzz" + port = 987654321 + sslEnabled = true + build() + } + assertThat(settings1.equals(settings2)).isFalse() + } + + @Test + @Suppress("ReplaceCallWithBinaryOperator") + fun `equals() should return false when port differs`() { + val settings1 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = 987654321 + sslEnabled = true + build() + } + val settings2 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = -1 + sslEnabled = true + build() + } + assertThat(settings1.equals(settings2)).isFalse() + } + + @Test + @Suppress("ReplaceCallWithBinaryOperator") + fun `equals() should return false when sslEnabled differs`() { + val settings1 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = 987654321 + sslEnabled = true + build() + } + val settings2 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = 987654321 + sslEnabled = false + build() + } + assertThat(settings1.equals(settings2)).isFalse() + } + + @Test + fun `hashCode() should return the same value when invoked on the same object`() { + val settings = FirebaseDataConnectSettings.defaultInstance + assertThat(settings.hashCode()).isEqualTo(settings.hashCode()) + } + + @Test + fun `hashCode() should return the same value when invoked on a distinct, but equal object`() { + val settings1 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = 987654321 + sslEnabled = true + build() + } + val settings2 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = 987654321 + sslEnabled = true + build() + } + // validate the assumption that `settings1` and `settings2` are distinct objects. + assertThat(settings1).isNotSameInstanceAs(settings2) + assertThat(settings1.hashCode()).isEqualTo(settings2.hashCode()) + } + + @Test + fun `hashCode() should return the different values when hostName differs`() { + val settings1 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + build() + } + val settings2 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "xyz987" + build() + } + assertThat(settings1.hashCode()).isNotEqualTo(settings2.hashCode()) + } + + @Test + fun `hashCode() should return the different values when port differs`() { + val settings1 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + port = 987 + build() + } + val settings2 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + port = 123 + build() + } + assertThat(settings1.hashCode()).isNotEqualTo(settings2.hashCode()) + } + + @Test + fun `hashCode() should return the different values when sslEnabled differs`() { + val settings1 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + sslEnabled = true + build() + } + val settings2 = + FirebaseDataConnectSettings.defaultInstance.builder.run { + sslEnabled = false + build() + } + assertThat(settings1.hashCode()).isNotEqualTo(settings2.hashCode()) + } + + @Test + fun `toString() should return a string that contains the property values`() { + val settings = + FirebaseDataConnectSettings.defaultInstance.builder.run { + hostName = "abc123" + port = 987654321 + sslEnabled = true + build() + } + val toStringResult = settings.toString() + assertThat(toStringResult).containsMatch("hostName=abc123\\W") + assertThat(toStringResult).containsMatch("port=987654321\\W") + assertThat(toStringResult).containsMatch("sslEnabled=true\\W") + } + + @Test + fun `dataConnectSettings() should use the default values`() { + dataConnectSettings { + assertThat(hostName).isEqualTo(FirebaseDataConnectSettings.defaultInstance.hostName) + assertThat(port).isEqualTo(FirebaseDataConnectSettings.defaultInstance.port) + assertThat(sslEnabled).isEqualTo(FirebaseDataConnectSettings.defaultInstance.sslEnabled) + } + } + + @Test + fun `dataConnectSettings() should create an object that uses the specified values`() { + val settings = dataConnectSettings { + hostName = "abc123" + port = 987654321 + sslEnabled = false + } + settings.apply { + assertThat(hostName).isEqualTo("abc123") + assertThat(port).isEqualTo(987654321) + assertThat(sslEnabled).isEqualTo(false) + } + } +} From cfa754d9ff87262f1e1058205e839995093d65a3 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 16 Oct 2023 16:12:18 -0400 Subject: [PATCH 012/573] google-services.json: fix syntax error --- firebase-dataconnect/google-services.json | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase-dataconnect/google-services.json b/firebase-dataconnect/google-services.json index b00b1a2d896..69bf5304db2 100644 --- a/firebase-dataconnect/google-services.json +++ b/firebase-dataconnect/google-services.json @@ -13,6 +13,7 @@ "package_name": "com.google.firebase.dataconnect" } } + } ], "configuration_version": "1" } From 175f6210561df3d395a46b36f2f2f1e18c54e81f Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 16 Oct 2023 16:13:00 -0400 Subject: [PATCH 013/573] FirebaseDataConnect.kt: get projectId from FirebaseApp --- .../dataconnect/FirebaseDataConnectTest.kt | 5 +---- .../firebase/dataconnect/FirebaseDataConnect.kt | 16 +++++++--------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index a92520024c2..e84646a72f7 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -19,7 +19,6 @@ import com.google.common.truth.Truth.assertThat import com.google.firebase.Firebase import com.google.firebase.app import com.google.firebase.initialize -import com.google.firebase.options import java.util.UUID import org.junit.Test import org.junit.runner.RunWith @@ -74,16 +73,14 @@ class FirebaseDataConnectTest { val dc = FirebaseDataConnect.instance dc.settings = dataConnectSettings { connectToEmulator() } - val projectId = "ZzyzxTestProject" val location = "ZzyzxTestLocation" dc.executeMutation( - projectId, location, "createPost", mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") ) - dc.executeQuery(projectId, location, "listPosts", emptyMap()) + dc.executeQuery(location, "listPosts", emptyMap()) } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index ca785b8b681..f3b3e8bbc70 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -98,16 +98,13 @@ internal constructor( } } - fun executeQuery( - projectId: String, - location: String, - operationName: String, - variables: Map - ): Struct { + fun executeQuery(location: String, operationName: String, variables: Map): Struct { val request = ExecuteQueryRequest.newBuilder().let { it.name = - "projects/${projectId}/locations/${location}/services/s/operationSets/crud/revisions/r" + "projects/${firebaseApp.options.projectId}" + + "/locations/${location}" + + "/services/s/operationSets/crud/revisions/r" it.operationName = operationName it.variables = structFromMap(variables) it.build() @@ -123,7 +120,6 @@ internal constructor( } fun executeMutation( - projectId: String, location: String, operationName: String, variables: Map @@ -131,7 +127,9 @@ internal constructor( val request = ExecuteMutationRequest.newBuilder().let { it.name = - "projects/${projectId}/locations/${location}/services/s/operationSets/crud/revisions/r" + "projects/${firebaseApp.options.projectId}" + + "/locations/${location}" + + "/services/s/operationSets/crud/revisions/r" it.operationName = operationName it.variables = Struct.newBuilder().run { From c5d7d18b2ea8bc54eabf10241585d60300989c7d Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 16 Oct 2023 21:59:47 -0400 Subject: [PATCH 014/573] google-services.json: add dummy api key --- firebase-dataconnect/google-services.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/firebase-dataconnect/google-services.json b/firebase-dataconnect/google-services.json index 69bf5304db2..c2298a9c2fa 100644 --- a/firebase-dataconnect/google-services.json +++ b/firebase-dataconnect/google-services.json @@ -12,7 +12,12 @@ "android_client_info": { "package_name": "com.google.firebase.dataconnect" } - } + }, + "api_key": [ + { + "current_key": "AIzayDNSXIbFmlXbIE6mCzDLQAqITYefhixbX4A" + } + ] } ], "configuration_version": "1" From d96ed01b8a2385e1b4da72d26739476cd431c22e Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 16 Oct 2023 22:00:40 -0400 Subject: [PATCH 015/573] add location and service to getInstance() --- .../dataconnect/FirebaseDataConnectTest.kt | 184 +++++++++++++++--- .../dataconnect/FirebaseDataConnect.kt | 140 +++++++++++-- .../FirebaseDataConnectRegistrar.kt | 12 +- .../dataconnect/FirebaseDataConnectTest.kt | 28 --- 4 files changed, 286 insertions(+), 78 deletions(-) delete mode 100644 firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index e84646a72f7..d1d5e52f6ea 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -16,61 +16,183 @@ package com.google.firebase.dataconnect import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.firebase.initialize import java.util.UUID +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock +import org.junit.After import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class FirebaseDataConnectTest { + private val createdFirebaseApps = CopyOnWriteArrayList() + + @After + fun deleteFirebaseApps() { + while (createdFirebaseApps.isNotEmpty()) { + createdFirebaseApps.removeAt(0).delete() + } + } + @Test - fun instance_should_return_a_non_null_instance() { - assertThat(FirebaseDataConnect.instance).isNotNull() + fun getInstance_without_specifying_an_app_should_use_the_default_app() { + val instance1 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation1", "TestService1") + val instance2 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation2", "TestService2") + // Validate the assumption that different location and service yield distinct instances. + assertThat(instance1).isNotSameInstanceAs(instance2) + + val instance1DefaultApp = FirebaseDataConnect.getInstance("TestLocation1", "TestService1") + val instance2DefaultApp = FirebaseDataConnect.getInstance("TestLocation2", "TestService2") + + assertThat(instance1DefaultApp).isSameInstanceAs(instance1) + assertThat(instance2DefaultApp).isSameInstanceAs(instance2) } @Test - fun instance_should_always_return_the_same_instance() { - val instance1 = FirebaseDataConnect.instance - val instance2 = FirebaseDataConnect.instance + fun getInstance_with_default_app_should_return_non_null() { + val instance = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") + assertThat(instance).isNotNull() + } + + @Test + fun getInstance_with_default_app_should_return_the_same_instance_every_time() { + val instance1 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") + val instance2 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") assertThat(instance1).isSameInstanceAs(instance2) } @Test - fun getInstance_with_default_app_should_return_same_instance_as_the_instance_getter() { - val instanceFromGetInstance = FirebaseDataConnect.getInstance(Firebase.app) - assertThat(instanceFromGetInstance).isSameInstanceAs(FirebaseDataConnect.instance) + fun getInstance_should_return_new_instance_after_terminate() { + val instance1 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") + instance1.terminate() + val instance2 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") + assertThat(instance1).isNotSameInstanceAs(instance2) } @Test - fun getInstance_with_non_default_app_should_return_non_default_instance() { + fun getInstance_should_return_distinct_instances_for_distinct_apps() { + val nonDefaultApp1 = createNonDefaultFirebaseApp() + val nonDefaultApp2 = createNonDefaultFirebaseApp() + val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp1, "TestLocation", "TestService") + val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp2, "TestLocation", "TestService") + assertThat(instance1).isNotSameInstanceAs(instance2) + } + + @Test + fun getInstance_should_return_distinct_instances_for_distinct_locations() { val nonDefaultApp = createNonDefaultFirebaseApp() - val nonDefaultInstanceFromGetInstance = FirebaseDataConnect.getInstance(nonDefaultApp) - assertThat(nonDefaultInstanceFromGetInstance).isNotSameInstanceAs(FirebaseDataConnect.instance) + val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService") + val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService") + assertThat(instance1).isNotSameInstanceAs(instance2) } @Test - fun getInstance_with_the_same_non_default_apps_should_return_the_same_instances() { + fun getInstance_should_return_distinct_instances_for_distinct_services() { val nonDefaultApp = createNonDefaultFirebaseApp() - val nonDefaultInstance1 = FirebaseDataConnect.getInstance(nonDefaultApp) - val nonDefaultInstance2 = FirebaseDataConnect.getInstance(nonDefaultApp) - assertThat(nonDefaultInstance1).isSameInstanceAs(nonDefaultInstance2) + val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService1") + val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService2") + assertThat(instance1).isNotSameInstanceAs(instance2) } @Test - fun getInstance_with_distinct_non_default_apps_should_return_distinct_instances() { - val nonDefaultApp1 = createNonDefaultFirebaseApp() - val nonDefaultApp2 = createNonDefaultFirebaseApp() - val nonDefaultInstance1 = FirebaseDataConnect.getInstance(nonDefaultApp1) - val nonDefaultInstance2 = FirebaseDataConnect.getInstance(nonDefaultApp2) - assertThat(nonDefaultInstance1).isNotSameInstanceAs(nonDefaultInstance2) + fun getInstance_should_return_a_new_instance_after_the_instance_is_terminated() { + val nonDefaultApp = createNonDefaultFirebaseApp() + val instance1A = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") + val instance2A = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService2") + assertThat(instance1A).isNotSameInstanceAs(instance2A) + + instance1A.terminate() + val instance1B = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") + assertThat(instance1A).isNotSameInstanceAs(instance1B) + assertThat(instance1A).isNotSameInstanceAs(instance2A) + + instance2A.terminate() + val instance2B = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService2") + assertThat(instance2A).isNotSameInstanceAs(instance2B) + assertThat(instance2A).isNotSameInstanceAs(instance1A) + assertThat(instance2A).isNotSameInstanceAs(instance1B) + } + + @Test + fun getInstance_should_be_thread_safe() { + val apps = + mutableListOf().run { + for (i in 0..4) { + add(createNonDefaultFirebaseApp()) + } + toList() + } + + val createdInstancesByThreadIdLock = ReentrantLock() + val createdInstancesByThreadId = mutableMapOf>() + val numThreads = 8 + + val threads = + mutableListOf().run { + val readyCountDown = AtomicInteger(numThreads) + for (i in 0 until numThreads) { + add( + Thread { + readyCountDown.decrementAndGet() + while (readyCountDown.get() > 0) { + /* spin */ + } + val instances = + mutableListOf().run { + for (app in apps) { + add(FirebaseDataConnect.getInstance(app, "TestLocation1", "TestService1")) + add(FirebaseDataConnect.getInstance(app, "TestLocation2", "TestService2")) + add(FirebaseDataConnect.getInstance(app, "TestLocation3", "TestService3")) + } + toList() + } + createdInstancesByThreadIdLock.withLock { createdInstancesByThreadId[i] = instances } + } + ) + } + toList() + } + + threads.forEach { it.start() } + threads.forEach { it.join() } + + assertThat(createdInstancesByThreadId.size).isEqualTo(8) + val expectedInstances = createdInstancesByThreadId.values.toList()[0] + assertThat(expectedInstances.size).isEqualTo(15) + createdInstancesByThreadId.keys.forEach { threadId -> + val createdInstances = createdInstancesByThreadId[threadId] + assertWithMessage("instances created by threadId=${threadId}") + .that(createdInstances) + .containsExactlyElementsIn(expectedInstances) + .inOrder() + } + } + + @Test + fun toString_should_return_a_string_that_contains_the_required_information() { + val app = createNonDefaultFirebaseApp() + val instance = + FirebaseDataConnect.getInstance(app = app, location = "TestLocation", service = "TestService") + + val toStringResult = instance.toString() + + assertThat(toStringResult).containsMatch("appName=${app.name}\\W") + assertThat(toStringResult).containsMatch("projectId=${app.options.projectId}\\W") + assertThat(toStringResult).containsMatch("location=TestLocation\\W") + assertThat(toStringResult).containsMatch("service=TestService\\W") } @Test fun helloWorld() { - val dc = FirebaseDataConnect.instance + val dc = FirebaseDataConnect.getInstance("TestLocation", "TestService") dc.settings = dataConnectSettings { connectToEmulator() } val location = "ZzyzxTestLocation" @@ -82,11 +204,15 @@ class FirebaseDataConnectTest { ) dc.executeQuery(location, "listPosts", emptyMap()) } -} -private fun createNonDefaultFirebaseApp() = - Firebase.initialize( - Firebase.app.applicationContext, - Firebase.app.options, - UUID.randomUUID().toString() - ) + private fun createNonDefaultFirebaseApp(): FirebaseApp { + val firebaseApp = + Firebase.initialize( + Firebase.app.applicationContext, + Firebase.app.options, + UUID.randomUUID().toString() + ) + createdFirebaseApps.add(firebaseApp) + return firebaseApp + } +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index f3b3e8bbc70..e04e9b860bc 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -13,9 +13,11 @@ // limitations under the License. package com.google.firebase.dataconnect +import android.content.Context import com.google.android.gms.security.ProviderInstaller import com.google.firebase.Firebase import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseAppLifecycleListener import com.google.firebase.app import com.google.protobuf.NullValue import com.google.protobuf.Struct @@ -28,25 +30,36 @@ import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read +import kotlin.concurrent.withLock import kotlin.concurrent.write import kotlinx.coroutines.CoroutineDispatcher class FirebaseDataConnect internal constructor( - private val firebaseApp: FirebaseApp, - private val backgroundDispatcher: CoroutineDispatcher, - private val blockingDispatcher: CoroutineDispatcher, + private val context: Context, + private val appName: String, + internal val projectId: String, + internal val location: String, + internal val service: String, + private val creator: FirebaseDataConnectFactory ) { private val logger = LoggerImpl("FirebaseDataConnect", Logger.Level.DEBUG) - private val settingsLock = ReentrantReadWriteLock() + private val lock = ReentrantReadWriteLock() private var settingsFrozen = false + private var terminated = false private val grpcChannel: ManagedChannel by lazy { // Make sure that no further changes can be made to the settings. - settingsLock.write { settingsFrozen = true } + lock.write { + if (terminated) { + throw IllegalStateException("instance has been terminated") + } + settingsFrozen = true + } // Upgrade the Android security provider using Google Play Services. // @@ -57,7 +70,7 @@ internal constructor( // If initialization fails for any reason, then a warning is logged and the original, // un-upgraded security provider is used. try { - ProviderInstaller.installIfNeeded(firebaseApp.applicationContext) + ProviderInstaller.installIfNeeded(context) } catch (e: Exception) { logger.warn(e) { "Failed to update ssl context" } } @@ -74,9 +87,7 @@ internal constructor( // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel to // respond more gracefully to network change events, such as switching from cellular to wifi. - AndroidChannelBuilder.usingBuilder(channelBuilder) - .context(firebaseApp.applicationContext) - .build() + AndroidChannelBuilder.usingBuilder(channelBuilder).context(context).build() } private val grpcStub: DataServiceBlockingStub by lazy { @@ -85,12 +96,15 @@ internal constructor( var settings: FirebaseDataConnectSettings = FirebaseDataConnectSettings.defaultInstance get() { - settingsLock.read { + lock.read { return field } } set(value) { - settingsLock.write { + lock.write { + if (terminated) { + throw IllegalStateException("instance has been terminated") + } if (settingsFrozen) { throw IllegalStateException("settings cannot be modified after they are used") } @@ -102,7 +116,7 @@ internal constructor( val request = ExecuteQueryRequest.newBuilder().let { it.name = - "projects/${firebaseApp.options.projectId}" + + "projects/${projectId}" + "/locations/${location}" + "/services/s/operationSets/crud/revisions/r" it.operationName = operationName @@ -127,7 +141,7 @@ internal constructor( val request = ExecuteMutationRequest.newBuilder().let { it.name = - "projects/${firebaseApp.options.projectId}" + + "projects/${projectId}" + "/locations/${location}" + "/services/s/operationSets/crud/revisions/r" it.operationName = operationName @@ -148,14 +162,25 @@ internal constructor( return response.data } + fun terminate() { + lock.write { + grpcChannel.shutdown() + terminated = true + creator.remove(this) + } + } + + override fun toString(): String { + return "FirebaseDataConnect" + + "{appName=$appName, projectId=$projectId, location=$location, service=$service}" + } + companion object { - @JvmStatic - val instance: FirebaseDataConnect - get() = getInstance(Firebase.app) + fun getInstance(location: String, service: String): FirebaseDataConnect = + getInstance(Firebase.app, location, service) - @JvmStatic - fun getInstance(app: FirebaseApp): FirebaseDataConnect = - app.get(FirebaseDataConnect::class.java) + fun getInstance(app: FirebaseApp, location: String, service: String): FirebaseDataConnect = + app.get(FirebaseDataConnectFactory::class.java).run { get(location, service) } } } @@ -177,3 +202,80 @@ private fun protoValueFromObject(obj: Any?): Value = } } .build() + +internal class FirebaseDataConnectFactory( + private val context: Context, + private val firebaseApp: FirebaseApp, + private val backgroundDispatcher: CoroutineDispatcher, + private val blockingDispatcher: CoroutineDispatcher, +) { + + private val firebaseAppLifecycleListener = FirebaseAppLifecycleListener { _, _ -> close() } + init { + firebaseApp.addLifecycleEventListener(firebaseAppLifecycleListener) + } + + private data class InstanceCacheKey( + val location: String, + val service: String, + ) + + private val lock = ReentrantLock() + private val instancesByCacheKey = mutableMapOf() + private var closed = false + + fun get(location: String, service: String): FirebaseDataConnect { + val key = InstanceCacheKey(location = location, service = service) + lock.withLock { + if (closed) { + throw IllegalStateException("FirebaseApp has been deleted") + } + val cachedInstance = instancesByCacheKey[key] + if (cachedInstance !== null) { + return cachedInstance + } + + val projectId = firebaseApp.options.projectId ?: "" + val newInstance = + FirebaseDataConnect( + context = context, + appName = firebaseApp.name, + projectId = projectId, + location = location, + service = service, + creator = this + ) + instancesByCacheKey[key] = newInstance + return newInstance + } + } + + fun remove(instance: FirebaseDataConnect) { + val key = InstanceCacheKey(location = instance.location, service = instance.service) + lock.withLock { + if (instancesByCacheKey[key] === instance) { + instancesByCacheKey.remove(key) + } + } + } + + private fun close() { + val instances = mutableListOf() + lock.withLock { + closed = true + instances.addAll(instancesByCacheKey.values) + } + + instances.forEach { instance -> instance.terminate() } + + lock.withLock { + if (instancesByCacheKey.isNotEmpty()) { + throw IllegalStateException( + "instances contains ${instances.size} instances " + + "after calling terminate() on all FirebaseDataConnect instances, " + + "but expected 0" + ) + } + } + } +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt index b69183e555e..7d0134b7385 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt @@ -14,7 +14,9 @@ package com.google.firebase.dataconnect +import android.content.Context import androidx.annotation.Keep +import androidx.annotation.RestrictTo import com.google.firebase.FirebaseApp import com.google.firebase.annotations.concurrent.Background import com.google.firebase.annotations.concurrent.Blocking @@ -32,16 +34,21 @@ import kotlinx.coroutines.CoroutineDispatcher * @hide */ @Keep +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) internal class FirebaseDataConnectRegistrar : ComponentRegistrar { + + @Keep override fun getComponents() = listOf( - Component.builder(FirebaseDataConnect::class.java) + Component.builder(FirebaseDataConnectFactory::class.java) .name(LIBRARY_NAME) .add(Dependency.required(firebaseApp)) + .add(Dependency.required(context)) .add(Dependency.required(backgroundDispatcher)) .add(Dependency.required(blockingDispatcher)) .factory { container -> - FirebaseDataConnect( + FirebaseDataConnectFactory( + container.get(context), container.get(firebaseApp), container.get(backgroundDispatcher), container.get(blockingDispatcher), @@ -55,6 +62,7 @@ internal class FirebaseDataConnectRegistrar : ComponentRegistrar { private const val LIBRARY_NAME = "fire-dataconnect" private val firebaseApp = unqualified(FirebaseApp::class.java) + private val context = unqualified(Context::class.java) private val backgroundDispatcher = qualified(Background::class.java, CoroutineDispatcher::class.java) private val blockingDispatcher = diff --git a/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt deleted file mode 100644 index 4501840384e..00000000000 --- a/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2023 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.dataconnect - -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class FirebaseDataConnectTest { - - @Test - fun `FirebaseDataConnectTest Hello World test`() { - System.out.println("FirebaseDataConnectTest Hello World test") - } -} From ebbbaf860085a6421aab39bd37a3da9d01469bcc Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 17 Oct 2023 13:03:46 -0400 Subject: [PATCH 016/573] DataConnectGrpcClient.kt added --- .../dataconnect/FirebaseDataConnectTest.kt | 5 +- .../dataconnect/DataConnectGrpcClient.kt | 141 ++++++++++++++++++ .../dataconnect/FirebaseDataConnect.kt | 129 ++++------------ 3 files changed, 172 insertions(+), 103 deletions(-) create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index d1d5e52f6ea..b67967b4375 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -195,14 +195,11 @@ class FirebaseDataConnectTest { val dc = FirebaseDataConnect.getInstance("TestLocation", "TestService") dc.settings = dataConnectSettings { connectToEmulator() } - val location = "ZzyzxTestLocation" - dc.executeMutation( - location, "createPost", mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") ) - dc.executeQuery(location, "listPosts", emptyMap()) + dc.executeQuery("listPosts", emptyMap()) } private fun createNonDefaultFirebaseApp(): FirebaseApp { diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt new file mode 100644 index 00000000000..441429473a1 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -0,0 +1,141 @@ +// Copyright 2023 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.dataconnect + +import android.content.Context +import com.google.android.gms.security.ProviderInstaller +import com.google.protobuf.NullValue +import com.google.protobuf.Struct +import com.google.protobuf.Value +import google.internal.firebase.firemat.v0.DataServiceGrpc +import google.internal.firebase.firemat.v0.DataServiceGrpc.DataServiceBlockingStub +import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteMutationRequest +import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteQueryRequest +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import io.grpc.android.AndroidChannelBuilder +import java.util.concurrent.TimeUnit + +internal class DataConnectGrpcClient( + val context: Context, + val projectId: String, + val location: String, + val service: String, + val hostName: String, + val port: Int, + val sslEnabled: Boolean +) { + private val logger = LoggerImpl("FirebaseDataConnectClient", Logger.Level.DEBUG) + + private val name = + "projects/${projectId}/locations/${location}/services/${service}/" + + "operationSets/crud/revisions/r" + + private val grpcChannel: ManagedChannel by lazy { + // Upgrade the Android security provider using Google Play Services. + // + // We need to upgrade the Security Provider before any network channels are initialized because + // okhttp maintains a list of supported providers that is initialized when the JVM first + // resolves the static dependencies of ManagedChannel. + // + // If initialization fails for any reason, then a warning is logged and the original, + // un-upgraded security provider is used. + try { + ProviderInstaller.installIfNeeded(context) + } catch (e: Exception) { + logger.warn(e) { "Failed to update ssl context" } + } + + val channelBuilder = ManagedChannelBuilder.forAddress(hostName, port) + if (!sslEnabled) { + channelBuilder.usePlaintext() + } + + // Ensure gRPC recovers from a dead connection. This is not typically necessary, as + // the OS will usually notify gRPC when a connection dies. But not always. This acts as a + // failsafe. + channelBuilder.keepAliveTime(30, TimeUnit.SECONDS) + + // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel to + // respond more gracefully to network change events, such as switching from cellular to wifi. + AndroidChannelBuilder.usingBuilder(channelBuilder).context(context).build() + } + + private val grpcStub: DataServiceBlockingStub by lazy { + DataServiceGrpc.newBlockingStub(grpcChannel) + } + + fun executeQuery(operationName: String, variables: Map): Struct { + val request = + ExecuteQueryRequest.newBuilder().let { + it.name = name + it.operationName = operationName + it.variables = structFromMap(variables) + it.build() + } + + logger.debug { "executeQuery() sending request: $request" } + val response = grpcStub.executeQuery(request) + logger.debug { "executeQuery() got response: $response" } + return response.data + } + + fun executeMutation(operationName: String, variables: Map): Struct { + val request = + ExecuteMutationRequest.newBuilder().let { + it.name = name + it.operationName = operationName + it.variables = + Struct.newBuilder().run { + putFields("data", Value.newBuilder().setStructValue(structFromMap(variables)).build()) + build() + } + it.build() + } + + logger.debug { "executeMutation() sending request: $request" } + val response = grpcStub.executeMutation(request) + logger.debug { "executeMutation() got response: $response" } + return response.data + } + + override fun toString(): String { + return "FirebaseDataConnectClient{" + + "projectId=$projectId, location=$location, service=$service, " + + "hostName=$hostName, port=$port, sslEnabled=$sslEnabled}" + } + + fun close() { + grpcChannel.shutdownNow() + } +} + +private fun structFromMap(map: Map): Struct = + Struct.newBuilder().run { + map.keys.sorted().forEach { key -> putFields(key, protoValueFromObject(map[key])) } + build() + } + +private fun protoValueFromObject(obj: Any?): Value = + Value.newBuilder() + .run { + when (obj) { + null -> setNullValue(NullValue.NULL_VALUE) + is String -> setStringValue(obj) + is Boolean -> setBoolValue(obj) + is Double -> setNumberValue(obj) + else -> throw IllegalArgumentException("unsupported value type: ${obj::class}") + } + } + .build() diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index e04e9b860bc..8affd4eabc8 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -14,7 +14,6 @@ package com.google.firebase.dataconnect import android.content.Context -import com.google.android.gms.security.ProviderInstaller import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseAppLifecycleListener @@ -22,14 +21,6 @@ import com.google.firebase.app import com.google.protobuf.NullValue import com.google.protobuf.Struct import com.google.protobuf.Value -import google.internal.firebase.firemat.v0.DataServiceGrpc -import google.internal.firebase.firemat.v0.DataServiceGrpc.DataServiceBlockingStub -import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteMutationRequest -import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteQueryRequest -import io.grpc.ManagedChannel -import io.grpc.ManagedChannelBuilder -import io.grpc.android.AndroidChannelBuilder -import java.util.concurrent.TimeUnit import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read @@ -52,48 +43,6 @@ internal constructor( private var settingsFrozen = false private var terminated = false - private val grpcChannel: ManagedChannel by lazy { - // Make sure that no further changes can be made to the settings. - lock.write { - if (terminated) { - throw IllegalStateException("instance has been terminated") - } - settingsFrozen = true - } - - // Upgrade the Android security provider using Google Play Services. - // - // We need to upgrade the Security Provider before any network channels are initialized because - // okhttp maintains a list of supported providers that is initialized when the JVM first - // resolves the static dependencies of ManagedChannel. - // - // If initialization fails for any reason, then a warning is logged and the original, - // un-upgraded security provider is used. - try { - ProviderInstaller.installIfNeeded(context) - } catch (e: Exception) { - logger.warn(e) { "Failed to update ssl context" } - } - - val channelBuilder = ManagedChannelBuilder.forAddress(settings.hostName, settings.port) - if (!settings.sslEnabled) { - channelBuilder.usePlaintext() - } - - // Ensure gRPC recovers from a dead connection. This is not typically necessary, as - // the OS will usually notify gRPC when a connection dies. But not always. This acts as a - // failsafe. - channelBuilder.keepAliveTime(30, TimeUnit.SECONDS) - - // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel to - // respond more gracefully to network change events, such as switching from cellular to wifi. - AndroidChannelBuilder.usingBuilder(channelBuilder).context(context).build() - } - - private val grpcStub: DataServiceBlockingStub by lazy { - DataServiceGrpc.newBlockingStub(grpcChannel) - } - var settings: FirebaseDataConnectSettings = FirebaseDataConnectSettings.defaultInstance get() { lock.read { @@ -112,59 +61,34 @@ internal constructor( } } - fun executeQuery(location: String, operationName: String, variables: Map): Struct { - val request = - ExecuteQueryRequest.newBuilder().let { - it.name = - "projects/${projectId}" + - "/locations/${location}" + - "/services/s/operationSets/crud/revisions/r" - it.operationName = operationName - it.variables = structFromMap(variables) - it.build() + private val grpcClint: DataConnectGrpcClient by lazy { + lock.write { + if (terminated) { + throw IllegalStateException("instance has been terminated") } + settingsFrozen = true - logger.debug { "executeQuery() sending request: $request" } - - val response = grpcStub.executeQuery(request) - - logger.debug { "executeQuery() got response: $response" } - - return response.data + DataConnectGrpcClient( + context = context, + projectId = projectId, + location = location, + service = service, + hostName = settings.hostName, + port = settings.port, + sslEnabled = settings.sslEnabled, + ) + } } - fun executeMutation( - location: String, - operationName: String, - variables: Map - ): Struct { - val request = - ExecuteMutationRequest.newBuilder().let { - it.name = - "projects/${projectId}" + - "/locations/${location}" + - "/services/s/operationSets/crud/revisions/r" - it.operationName = operationName - it.variables = - Struct.newBuilder().run { - putFields("data", Value.newBuilder().setStructValue(structFromMap(variables)).build()) - build() - } - it.build() - } - - logger.debug { "executeMutation() sending request: $request" } + fun executeQuery(operationName: String, variables: Map): Struct = + grpcClint.executeQuery(operationName, variables) - val response = grpcStub.executeMutation(request) - - logger.debug { "executeMutation() got response: $response" } - - return response.data - } + fun executeMutation(operationName: String, variables: Map): Struct = + grpcClint.executeMutation(operationName, variables) fun terminate() { lock.write { - grpcChannel.shutdown() + grpcClint.close() terminated = true creator.remove(this) } @@ -251,10 +175,17 @@ internal class FirebaseDataConnectFactory( } fun remove(instance: FirebaseDataConnect) { - val key = InstanceCacheKey(location = instance.location, service = instance.service) lock.withLock { - if (instancesByCacheKey[key] === instance) { - instancesByCacheKey.remove(key) + val entries = instancesByCacheKey.entries.filter { it.value === instance } + if (entries.isEmpty()) { + return + } else if (entries.size == 1) { + instancesByCacheKey.remove(entries[0].key) + } else { + throw IllegalStateException( + "internal error: FirebaseDataConnect instance $instance" + + "maps to more than one key: ${entries.map { it.key }.joinToString(", ")}" + ) } } } From 648c912267e11ed406aa1688bfd8c827d0250f8b Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 17 Oct 2023 13:15:10 -0400 Subject: [PATCH 017/573] FirebaseDataConnectFactory.kt added --- .../dataconnect/FirebaseDataConnect.kt | 129 ++---------------- .../dataconnect/FirebaseDataConnectFactory.kt | 105 ++++++++++++++ 2 files changed, 117 insertions(+), 117 deletions(-) create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 8affd4eabc8..51b410c5453 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -16,17 +16,11 @@ package com.google.firebase.dataconnect import android.content.Context import com.google.firebase.Firebase import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseAppLifecycleListener import com.google.firebase.app -import com.google.protobuf.NullValue import com.google.protobuf.Struct -import com.google.protobuf.Value -import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read -import kotlin.concurrent.withLock import kotlin.concurrent.write -import kotlinx.coroutines.CoroutineDispatcher class FirebaseDataConnect internal constructor( @@ -59,9 +53,11 @@ internal constructor( } field = value } + logger.debug { "Settings changed to $value" } } private val grpcClint: DataConnectGrpcClient by lazy { + logger.debug { "DataConnectGrpcClient initialization started" } lock.write { if (terminated) { throw IllegalStateException("instance has been terminated") @@ -69,14 +65,15 @@ internal constructor( settingsFrozen = true DataConnectGrpcClient( - context = context, - projectId = projectId, - location = location, - service = service, - hostName = settings.hostName, - port = settings.port, - sslEnabled = settings.sslEnabled, - ) + context = context, + projectId = projectId, + location = location, + service = service, + hostName = settings.hostName, + port = settings.port, + sslEnabled = settings.sslEnabled, + ) + .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } } } @@ -87,6 +84,7 @@ internal constructor( grpcClint.executeMutation(operationName, variables) fun terminate() { + logger.debug { "terminate() called" } lock.write { grpcClint.close() terminated = true @@ -107,106 +105,3 @@ internal constructor( app.get(FirebaseDataConnectFactory::class.java).run { get(location, service) } } } - -private fun structFromMap(map: Map): Struct = - Struct.newBuilder().run { - map.keys.sorted().forEach { key -> putFields(key, protoValueFromObject(map[key])) } - build() - } - -private fun protoValueFromObject(obj: Any?): Value = - Value.newBuilder() - .run { - when (obj) { - null -> setNullValue(NullValue.NULL_VALUE) - is String -> setStringValue(obj) - is Boolean -> setBoolValue(obj) - is Double -> setNumberValue(obj) - else -> throw IllegalArgumentException("unsupported value type: ${obj::class}") - } - } - .build() - -internal class FirebaseDataConnectFactory( - private val context: Context, - private val firebaseApp: FirebaseApp, - private val backgroundDispatcher: CoroutineDispatcher, - private val blockingDispatcher: CoroutineDispatcher, -) { - - private val firebaseAppLifecycleListener = FirebaseAppLifecycleListener { _, _ -> close() } - init { - firebaseApp.addLifecycleEventListener(firebaseAppLifecycleListener) - } - - private data class InstanceCacheKey( - val location: String, - val service: String, - ) - - private val lock = ReentrantLock() - private val instancesByCacheKey = mutableMapOf() - private var closed = false - - fun get(location: String, service: String): FirebaseDataConnect { - val key = InstanceCacheKey(location = location, service = service) - lock.withLock { - if (closed) { - throw IllegalStateException("FirebaseApp has been deleted") - } - val cachedInstance = instancesByCacheKey[key] - if (cachedInstance !== null) { - return cachedInstance - } - - val projectId = firebaseApp.options.projectId ?: "" - val newInstance = - FirebaseDataConnect( - context = context, - appName = firebaseApp.name, - projectId = projectId, - location = location, - service = service, - creator = this - ) - instancesByCacheKey[key] = newInstance - return newInstance - } - } - - fun remove(instance: FirebaseDataConnect) { - lock.withLock { - val entries = instancesByCacheKey.entries.filter { it.value === instance } - if (entries.isEmpty()) { - return - } else if (entries.size == 1) { - instancesByCacheKey.remove(entries[0].key) - } else { - throw IllegalStateException( - "internal error: FirebaseDataConnect instance $instance" + - "maps to more than one key: ${entries.map { it.key }.joinToString(", ")}" - ) - } - } - } - - private fun close() { - val instances = mutableListOf() - lock.withLock { - closed = true - instances.addAll(instancesByCacheKey.values) - } - - instances.forEach { instance -> instance.terminate() } - - lock.withLock { - if (instancesByCacheKey.isNotEmpty()) { - throw IllegalStateException( - "instances contains ${instances.size} instances " + - "after calling terminate() on all FirebaseDataConnect instances, " + - "but expected 0" - ) - } - } - } -} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt new file mode 100644 index 00000000000..5a3a5bfe180 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt @@ -0,0 +1,105 @@ +// Copyright 2023 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.dataconnect + +import android.content.Context +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseAppLifecycleListener +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock +import kotlinx.coroutines.CoroutineDispatcher + +internal class FirebaseDataConnectFactory( + private val context: Context, + private val firebaseApp: FirebaseApp, + private val backgroundDispatcher: CoroutineDispatcher, + private val blockingDispatcher: CoroutineDispatcher, +) { + + private val firebaseAppLifecycleListener = FirebaseAppLifecycleListener { _, _ -> close() } + init { + firebaseApp.addLifecycleEventListener(firebaseAppLifecycleListener) + } + + private data class InstanceCacheKey( + val location: String, + val service: String, + ) + + private val lock = ReentrantLock() + private val instancesByCacheKey = mutableMapOf() + private var closed = false + + fun get(location: String, service: String): FirebaseDataConnect { + val key = InstanceCacheKey(location = location, service = service) + lock.withLock { + if (closed) { + throw IllegalStateException("FirebaseApp has been deleted") + } + val cachedInstance = instancesByCacheKey[key] + if (cachedInstance !== null) { + return cachedInstance + } + + val projectId = firebaseApp.options.projectId ?: "" + val newInstance = + FirebaseDataConnect( + context = context, + appName = firebaseApp.name, + projectId = projectId, + location = location, + service = service, + creator = this + ) + instancesByCacheKey[key] = newInstance + return newInstance + } + } + + fun remove(instance: FirebaseDataConnect) { + lock.withLock { + val entries = instancesByCacheKey.entries.filter { it.value === instance } + if (entries.isEmpty()) { + return + } else if (entries.size == 1) { + instancesByCacheKey.remove(entries[0].key) + } else { + throw IllegalStateException( + "internal error: FirebaseDataConnect instance $instance" + + "maps to more than one key: ${entries.map { it.key }.joinToString(", ")}" + ) + } + } + } + + private fun close() { + val instances = mutableListOf() + lock.withLock { + closed = true + instances.addAll(instancesByCacheKey.values) + } + + instances.forEach { instance -> instance.terminate() } + + lock.withLock { + if (instancesByCacheKey.isNotEmpty()) { + throw IllegalStateException( + "instances contains ${instances.size} instances " + + "after calling terminate() on all FirebaseDataConnect instances, " + + "but expected 0" + ) + } + } + } +} From e144476632c38204fbfe09fff92b0cbbb3f15171 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 17 Oct 2023 13:23:30 -0400 Subject: [PATCH 018/573] paramaterize revision --- .../dataconnect/FirebaseDataConnectTest.kt | 9 ++++++--- .../dataconnect/DataConnectGrpcClient.kt | 20 +++++++++++-------- .../dataconnect/FirebaseDataConnect.kt | 11 ++++++---- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index b67967b4375..eaffb901e11 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -196,10 +196,13 @@ class FirebaseDataConnectTest { dc.settings = dataConnectSettings { connectToEmulator() } dc.executeMutation( - "createPost", - mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") + revision = "TestRevision", + operationName = "createPost", + variables = + mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") ) - dc.executeQuery("listPosts", emptyMap()) + + dc.executeQuery(revision = "TestRevision", operationName = "listPosts", variables = emptyMap()) } private fun createNonDefaultFirebaseApp(): FirebaseApp { diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 441429473a1..95a7da278cb 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -38,10 +38,6 @@ internal class DataConnectGrpcClient( ) { private val logger = LoggerImpl("FirebaseDataConnectClient", Logger.Level.DEBUG) - private val name = - "projects/${projectId}/locations/${location}/services/${service}/" + - "operationSets/crud/revisions/r" - private val grpcChannel: ManagedChannel by lazy { // Upgrade the Android security provider using Google Play Services. // @@ -76,10 +72,10 @@ internal class DataConnectGrpcClient( DataServiceGrpc.newBlockingStub(grpcChannel) } - fun executeQuery(operationName: String, variables: Map): Struct { + fun executeQuery(revision: String, operationName: String, variables: Map): Struct { val request = ExecuteQueryRequest.newBuilder().let { - it.name = name + it.name = nameForRevision(revision) it.operationName = operationName it.variables = structFromMap(variables) it.build() @@ -91,10 +87,14 @@ internal class DataConnectGrpcClient( return response.data } - fun executeMutation(operationName: String, variables: Map): Struct { + fun executeMutation( + revision: String, + operationName: String, + variables: Map + ): Struct { val request = ExecuteMutationRequest.newBuilder().let { - it.name = name + it.name = nameForRevision(revision) it.operationName = operationName it.variables = Struct.newBuilder().run { @@ -119,6 +119,10 @@ internal class DataConnectGrpcClient( fun close() { grpcChannel.shutdownNow() } + + private fun nameForRevision(revision: String): String = + "projects/$projectId/locations/$location/services/$service/" + + "operationSets/crud/revisions/$revision" } private fun structFromMap(map: Map): Struct = diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 51b410c5453..d6267c87569 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -77,11 +77,14 @@ internal constructor( } } - fun executeQuery(operationName: String, variables: Map): Struct = - grpcClint.executeQuery(operationName, variables) + fun executeQuery(revision: String, operationName: String, variables: Map): Struct = + grpcClint.executeQuery(revision, operationName, variables) - fun executeMutation(operationName: String, variables: Map): Struct = - grpcClint.executeMutation(operationName, variables) + fun executeMutation( + revision: String, + operationName: String, + variables: Map + ): Struct = grpcClint.executeMutation(revision, operationName, variables) fun terminate() { logger.debug { "terminate() called" } From e394992b7e6f111b671e5ba189d01b069f03e835 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 20 Oct 2023 15:14:06 -0400 Subject: [PATCH 019/573] firebase-dataconnect.gradle -> firebase-dataconnect.gradle.kts (#504) --- .../firebase-dataconnect.gradle | 156 ------------------ .../firebase-dataconnect.gradle.kts | 101 ++++++++++++ firebase-dataconnect/proguard.txt | 18 -- .../firematdata/v0/data_service.proto | 0 .../firematdata/v0/data_service_stream.proto | 0 gradle/libs.versions.toml | 9 + 6 files changed, 110 insertions(+), 174 deletions(-) delete mode 100644 firebase-dataconnect/firebase-dataconnect.gradle create mode 100644 firebase-dataconnect/firebase-dataconnect.gradle.kts delete mode 100644 firebase-dataconnect/proguard.txt rename firebase-dataconnect/src/{ => main}/proto/google/internal/firebase/firematdata/v0/data_service.proto (100%) rename firebase-dataconnect/src/{ => main}/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto (100%) diff --git a/firebase-dataconnect/firebase-dataconnect.gradle b/firebase-dataconnect/firebase-dataconnect.gradle deleted file mode 100644 index eaab7cefcec..00000000000 --- a/firebase-dataconnect/firebase-dataconnect.gradle +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2023 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. - -plugins { - id 'firebase-library' - id("kotlin-android") - id 'com.google.protobuf' -} - -firebaseLibrary { - libraryGroup "dataconnect" - publishSources = true - testLab { - enabled = true - timeout = '30m' - } -} - -protobuf { - // Configure the protoc executable - protoc { - // Download from repositories - artifact = "com.google.protobuf:protoc:$protocVersion" - } - plugins { - grpc { - artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" - } - } - generateProtoTasks { - all().each { task -> - task.builtins { - java { option 'lite' } - } - task.plugins { - grpc { - option 'lite' - } - } - } - } -} - -android { - adbOptions { - timeOutInMs 60 * 1000 - } - - namespace "com.google.firebase.dataconnect" - compileSdkVersion project.targetSdkVersion - defaultConfig { - targetSdkVersion project.targetSdkVersion - minSdkVersion 21 - versionName version - multiDexEnabled true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'proguard.txt' - } - - sourceSets { - main { - proto { - srcDir 'src/proto' - } - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - testOptions.unitTests.includeAndroidResources = true - -} - -tasks.withType(Test) { - maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 -} - -googleJavaFormat { -} - -dependencies { - androidTestAnnotationProcessor 'com.google.auto.value:auto-value:1.6.5' - androidTestImplementation "androidx.annotation:annotation:1.1.0" - androidTestImplementation "androidx.test.ext:junit:$androidxTestJUnitVersion" - androidTestImplementation 'androidx.test:rules:1.5.0' - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' - androidTestImplementation 'junit:junit:4.13.2' - androidTestImplementation 'org.mockito:mockito-android:2.25.0' - androidTestImplementation 'org.mockito:mockito-core:2.25.0' - androidTestImplementation("com.google.truth:truth:$googleTruthVersion") { - exclude group: "org.codehaus.mojo", module: "animal-sniffer-annotations" - } - annotationProcessor 'com.google.auto.value:auto-value:1.6.5' - compileOnly 'com.google.auto.value:auto-value-annotations:1.6.6' - compileOnly 'javax.annotation:jsr250-api:1.0' - implementation "io.grpc:grpc-android:$grpcVersion" - implementation "io.grpc:grpc-okhttp:$grpcVersion" - implementation "io.grpc:grpc-protobuf-lite:$grpcVersion" - implementation "io.grpc:grpc-stub:$grpcVersion" - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" - implementation 'androidx.annotation:annotation:1.1.0' - implementation 'com.google.android.gms:play-services-base:18.0.1' - implementation 'com.google.android.gms:play-services-basement:18.1.0' - implementation 'com.google.android.gms:play-services-tasks:18.0.1' - implementation 'com.google.firebase:firebase-annotations:16.2.0' - implementation 'com.google.firebase:firebase-appcheck-interop:17.0.0' - implementation 'com.google.firebase:firebase-database-collection:18.0.1' - implementation project(':protolite-well-known-types') - implementation('com.google.firebase:firebase-auth-interop:19.0.2') { - exclude group: "com.google.firebase", module: "firebase-common" - } - implementation(project(":firebase-common")) - implementation(project(":firebase-common:ktx")) - implementation(project(":firebase-components")) - javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' - testCompileOnly "com.google.protobuf:protobuf-java:$protocVersion" - testImplementation "androidx.test:core:$androidxTestCoreVersion" - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0" - testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' - testImplementation 'com.google.android.gms:play-services-tasks:18.0.1' - testImplementation 'com.google.guava:guava-testlib:12.0-rc2' - testImplementation 'junit:junit:4.12' - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:2.25.0' - testImplementation project(':firebase-database-collection') - testImplementation project(':firebase-dataconnect') -} - -gradle.projectsEvaluated { - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked" << "-Werror" - } -} - -// ========================================================================== -// Copy from here down if you want to use the google-services plugin in your -// androidTest integration tests. -// ========================================================================== -ext.packageName = "com.google.firebase.dataconnect" -apply from: '../gradle/googleServices.gradle' diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts new file mode 100644 index 00000000000..1a824a243b1 --- /dev/null +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -0,0 +1,101 @@ +// Copyright 2023 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. + +plugins { + id("firebase-library") + id("kotlin-android") + id("com.google.protobuf") +} + +firebaseLibrary { + publishSources = true + publishJavadoc = true +} + +android { + val targetSdkVersion: Int by rootProject + + namespace = "com.google.firebase.dataconnect" + compileSdk = 33 + defaultConfig { + minSdk = 21 + targetSdk = targetSdkVersion + multiDexEnabled = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { jvmTarget = "1.8" } +} + +protobuf { + protoc { + artifact = "${libs.protoc.get()}" + } + plugins { + create("java") { + artifact = "${libs.grpc.protoc.gen.java.get()}" + } + create("grpc") { + artifact = "${libs.grpc.protoc.gen.java.get()}" + } + } + generateProtoTasks { + all().forEach { task -> + task.builtins { + create("java") { + option("lite") + } + } + task.plugins { + create("grpc") { + option("lite") + } + } + } + } +} + +dependencies { + api(project(":firebase-common")) + api(project(":firebase-common:ktx")) + + api(libs.kotlinx.coroutines.core) + + implementation(project(":firebase-annotations")) + implementation(project(":firebase-components")) + implementation(project(":protolite-well-known-types")) + + compileOnly(libs.javax.annotation.jsr250) + implementation(libs.grpc.android) + implementation(libs.grpc.okhttp) + implementation(libs.grpc.protobuf.lite) + implementation(libs.grpc.stub) + implementation(libs.protobuf.javalite) + + testCompileOnly(libs.protobuf.java) + testImplementation(libs.robolectric) + testImplementation(libs.truth) + + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.truth) +} + +extra["packageName"] = "com.google.firebase.dataconnect" +apply(from = "../gradle/googleServices.gradle") diff --git a/firebase-dataconnect/proguard.txt b/firebase-dataconnect/proguard.txt deleted file mode 100644 index b76f1281567..00000000000 --- a/firebase-dataconnect/proguard.txt +++ /dev/null @@ -1,18 +0,0 @@ -# Needed for DNS resolution. Present in OpenJDK, but not Android --dontwarn javax.naming.** - -# Don't warn about checkerframework -# -# Guava uses the checkerframework and the annotations -# can safely be ignored at runtime. --dontwarn org.checkerframework.** - -# Guava warnings: --dontwarn java.lang.ClassValue --dontwarn com.google.j2objc.annotations.Weak --dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement --dontwarn javax.lang.model.element.Modifier - -# Okhttp warnings. --dontwarn okio.** --dontwarn com.google.j2objc.annotations.** diff --git a/firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service.proto b/firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service.proto similarity index 100% rename from firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service.proto rename to firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service.proto diff --git a/firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto b/firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto similarity index 100% rename from firebase-dataconnect/src/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto rename to firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e2c7a96d8ef..ac1ef65061a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,12 @@ dagger-dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version = "2.9.0" } findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version = "3.0.2" } +grpc-android = { module = "io.grpc:grpc-android", version.ref = "grpc" } +grpc-okhttp = { module = "io.grpc:grpc-okhttp", version.ref = "grpc" } +grpc-protobuf-lite = { module = "io.grpc:grpc-protobuf-lite", version.ref = "grpc" } +grpc-protoc-gen-java = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" } +grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } +javax-annotation-jsr250 = { module = "javax.annotation:jsr250-api", version = "1.0" } javax-inject = { module = "javax.inject:javax.inject", version = "1" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-coroutines-tasks = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutines" } @@ -42,6 +48,9 @@ org-json = { module = "org.json:json", version = "20210307" } playservices-base = { module = "com.google.android.gms:play-services-base", version = "18.1.0" } playservices-basement = { module = "com.google.android.gms:play-services-basement", version = "18.1.0" } playservices-tasks = { module = "com.google.android.gms:play-services-tasks", version = "18.0.2" } +protoc = { module = "com.google.protobuf:protoc", version.ref = "protoc" } +protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "javalite" } +protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "javalite" } # Test libs androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } From 1ea620eec264a8e12ffc90604afad26c49dacd5c Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 20 Oct 2023 16:49:03 -0400 Subject: [PATCH 020/573] Use grpc-kotlin instead of grpc-java (#505) --- .../firebase-dataconnect.gradle.kts | 22 +++++- .../dataconnect/FirebaseDataConnectTest.kt | 3 +- .../dataconnect/DataConnectGrpcClient.kt | 78 ++++++++++--------- .../dataconnect/FirebaseDataConnect.kt | 9 ++- gradle/libs.versions.toml | 21 +++-- 5 files changed, 82 insertions(+), 51 deletions(-) diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts index 1a824a243b1..bae5e4041ba 100644 --- a/firebase-dataconnect/firebase-dataconnect.gradle.kts +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -52,18 +52,27 @@ protobuf { create("grpc") { artifact = "${libs.grpc.protoc.gen.java.get()}" } + create("grpckt") { + artifact = "${libs.grpc.protoc.gen.kotlin.get()}:jdk8@jar" + } } generateProtoTasks { all().forEach { task -> task.builtins { - create("java") { + create("kotlin") { option("lite") } } task.plugins { + create("java") { + option("lite") + } create("grpc") { option("lite") } + create("grpckt") { + option("lite") + } } } } @@ -83,8 +92,10 @@ dependencies { implementation(libs.grpc.android) implementation(libs.grpc.okhttp) implementation(libs.grpc.protobuf.lite) + implementation(libs.grpc.kotlin.stub) implementation(libs.grpc.stub) - implementation(libs.protobuf.javalite) + implementation(libs.protobuf.java.lite) + implementation(libs.protobuf.kotlin.lite) testCompileOnly(libs.protobuf.java) testImplementation(libs.robolectric) @@ -94,8 +105,15 @@ dependencies { androidTestImplementation(libs.androidx.test.junit) androidTestImplementation(libs.androidx.test.rules) androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.kotlin.coroutines.test) androidTestImplementation(libs.truth) } +tasks.withType().all { + kotlinOptions { + freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn") + } +} + extra["packageName"] = "com.google.firebase.dataconnect" apply(from = "../gradle/googleServices.gradle") diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index eaffb901e11..df648470751 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -26,6 +26,7 @@ import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Test import org.junit.runner.RunWith @@ -191,7 +192,7 @@ class FirebaseDataConnectTest { } @Test - fun helloWorld() { + fun helloWorld() = runTest { val dc = FirebaseDataConnect.getInstance("TestLocation", "TestService") dc.settings = dataConnectSettings { connectToEmulator() } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 95a7da278cb..cd8f3fe19ce 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -18,14 +18,17 @@ import com.google.android.gms.security.ProviderInstaller import com.google.protobuf.NullValue import com.google.protobuf.Struct import com.google.protobuf.Value -import google.internal.firebase.firemat.v0.DataServiceGrpc -import google.internal.firebase.firemat.v0.DataServiceGrpc.DataServiceBlockingStub -import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteMutationRequest -import google.internal.firebase.firemat.v0.DataServiceOuterClass.ExecuteQueryRequest +import com.google.protobuf.struct +import com.google.protobuf.value +import google.internal.firebase.firemat.v0.DataServiceGrpcKt.DataServiceCoroutineStub +import google.internal.firebase.firemat.v0.executeMutationRequest +import google.internal.firebase.firemat.v0.executeQueryRequest import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder import java.util.concurrent.TimeUnit +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor internal class DataConnectGrpcClient( val context: Context, @@ -53,33 +56,38 @@ internal class DataConnectGrpcClient( logger.warn(e) { "Failed to update ssl context" } } - val channelBuilder = ManagedChannelBuilder.forAddress(hostName, port) - if (!sslEnabled) { - channelBuilder.usePlaintext() - } + ManagedChannelBuilder.forAddress(hostName, port).let { + if (!sslEnabled) { + it.usePlaintext() + } - // Ensure gRPC recovers from a dead connection. This is not typically necessary, as - // the OS will usually notify gRPC when a connection dies. But not always. This acts as a - // failsafe. - channelBuilder.keepAliveTime(30, TimeUnit.SECONDS) + // Ensure gRPC recovers from a dead connection. This is not typically necessary, as + // the OS will usually notify gRPC when a connection dies. But not always. This acts as a + // failsafe. + it.keepAliveTime(30, TimeUnit.SECONDS) - // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel to - // respond more gracefully to network change events, such as switching from cellular to wifi. - AndroidChannelBuilder.usingBuilder(channelBuilder).context(context).build() - } + // TODO: Create a dedicated executor rather than using a global one. + // See go/kotlin/coroutines/coroutine-contexts-scopes.md for details. + it.executor(Dispatchers.IO.asExecutor()) - private val grpcStub: DataServiceBlockingStub by lazy { - DataServiceGrpc.newBlockingStub(grpcChannel) + // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel to + // respond more gracefully to network change events, such as switching from cellular to wifi. + AndroidChannelBuilder.usingBuilder(it).context(context).build() + } } - fun executeQuery(revision: String, operationName: String, variables: Map): Struct { - val request = - ExecuteQueryRequest.newBuilder().let { - it.name = nameForRevision(revision) - it.operationName = operationName - it.variables = structFromMap(variables) - it.build() - } + private val grpcStub: DataServiceCoroutineStub by lazy { DataServiceCoroutineStub(grpcChannel) } + + suspend fun executeQuery( + revision: String, + operationName: String, + variables: Map + ): Struct { + val request = executeQueryRequest { + this.name = nameForRevision(revision) + this.operationName = operationName + this.variables = structFromMap(variables) + } logger.debug { "executeQuery() sending request: $request" } val response = grpcStub.executeQuery(request) @@ -87,22 +95,18 @@ internal class DataConnectGrpcClient( return response.data } - fun executeMutation( + suspend fun executeMutation( revision: String, operationName: String, variables: Map ): Struct { - val request = - ExecuteMutationRequest.newBuilder().let { - it.name = nameForRevision(revision) - it.operationName = operationName - it.variables = - Struct.newBuilder().run { - putFields("data", Value.newBuilder().setStructValue(structFromMap(variables)).build()) - build() - } - it.build() + val request = executeMutationRequest { + this.name = nameForRevision(revision) + this.operationName = operationName + this.variables = struct { + this.fields.put("data", value { structValue = structFromMap(variables) }) } + } logger.debug { "executeMutation() sending request: $request" } val response = grpcStub.executeMutation(request) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index d6267c87569..936cf52fa41 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -77,10 +77,13 @@ internal constructor( } } - fun executeQuery(revision: String, operationName: String, variables: Map): Struct = - grpcClint.executeQuery(revision, operationName, variables) + suspend fun executeQuery( + revision: String, + operationName: String, + variables: Map + ): Struct = grpcClint.executeQuery(revision, operationName, variables) - fun executeMutation( + suspend fun executeMutation( revision: String, operationName: String, variables: Map diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac1ef65061a..d1811f353d8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,19 @@ [versions] # javalite, protoc and protobufjavautil versions should be in sync while updating and -# it needs to match the protobuf version which grpc has transitive dependency on. +# it needs to match the protobuf version which grpc has transitive dependency on, which +# needs to match the version of grpc that grpc-kotlin has a transitive dependency on. android-lint = "30.3.1" autovalue = "1.10.1" -coroutines = "1.6.4" +coroutines = "1.7.3" dagger = "2.43.2" -grpc = "1.52.1" -javalite = "3.21.11" -kotlin = "1.7.10" -protoc = "3.21.11" +grpc = "1.57.2" +grpcKotlin = "1.4.0" +javalite = "3.24.0" +kotlin = "1.8.0" +protoc = "3.24.0" robolectric = "4.10.3" truth = "1.1.2" -protobufjavautil = "3.21.11" +protobufjavautil = "3.24.0" kotest = "5.5.5" quickcheck = "0.6" androidx-test-core="1.5.0" @@ -33,9 +35,11 @@ dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version = "2.9.0" } findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version = "3.0.2" } grpc-android = { module = "io.grpc:grpc-android", version.ref = "grpc" } +grpc-kotlin-stub = { module = "io.grpc:grpc-kotlin-stub", version.ref = "grpcKotlin" } grpc-okhttp = { module = "io.grpc:grpc-okhttp", version.ref = "grpc" } grpc-protobuf-lite = { module = "io.grpc:grpc-protobuf-lite", version.ref = "grpc" } grpc-protoc-gen-java = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" } +grpc-protoc-gen-kotlin = { module = "io.grpc:protoc-gen-grpc-kotlin", version.ref = "grpcKotlin" } grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } javax-annotation-jsr250 = { module = "javax.annotation:jsr250-api", version = "1.0" } javax-inject = { module = "javax.inject:javax.inject", version = "1" } @@ -50,7 +54,8 @@ playservices-basement = { module = "com.google.android.gms:play-services-basemen playservices-tasks = { module = "com.google.android.gms:play-services-tasks", version = "18.0.2" } protoc = { module = "com.google.protobuf:protoc", version.ref = "protoc" } protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "javalite" } -protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "javalite" } +protobuf-java-lite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "javalite" } +protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "javalite" } # Test libs androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } From e66391f0e5b6932c19f7d001fd5da78a1b7dc4d2 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 20 Oct 2023 17:02:44 -0400 Subject: [PATCH 021/573] FirebaseDataConnect.kt: make it Closeable --- .../dataconnect/FirebaseDataConnectTest.kt | 6 ++--- .../dataconnect/FirebaseDataConnect.kt | 26 +++++++++++-------- .../dataconnect/FirebaseDataConnectFactory.kt | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index df648470751..81462c96543 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -73,7 +73,7 @@ class FirebaseDataConnectTest { @Test fun getInstance_should_return_new_instance_after_terminate() { val instance1 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") - instance1.terminate() + instance1.close() val instance2 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") assertThat(instance1).isNotSameInstanceAs(instance2) } @@ -110,12 +110,12 @@ class FirebaseDataConnectTest { val instance2A = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService2") assertThat(instance1A).isNotSameInstanceAs(instance2A) - instance1A.terminate() + instance1A.close() val instance1B = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") assertThat(instance1A).isNotSameInstanceAs(instance1B) assertThat(instance1A).isNotSameInstanceAs(instance2A) - instance2A.terminate() + instance2A.close() val instance2B = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService2") assertThat(instance2A).isNotSameInstanceAs(instance2B) assertThat(instance2A).isNotSameInstanceAs(instance1A) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 936cf52fa41..fc59aac2c75 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -18,6 +18,7 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.protobuf.Struct +import java.io.Closeable import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read import kotlin.concurrent.write @@ -30,12 +31,12 @@ internal constructor( internal val location: String, internal val service: String, private val creator: FirebaseDataConnectFactory -) { +) : Closeable { private val logger = LoggerImpl("FirebaseDataConnect", Logger.Level.DEBUG) private val lock = ReentrantReadWriteLock() private var settingsFrozen = false - private var terminated = false + private var closed = false var settings: FirebaseDataConnectSettings = FirebaseDataConnectSettings.defaultInstance get() { @@ -45,8 +46,8 @@ internal constructor( } set(value) { lock.write { - if (terminated) { - throw IllegalStateException("instance has been terminated") + if (closed) { + throw IllegalStateException("instance has been closed") } if (settingsFrozen) { throw IllegalStateException("settings cannot be modified after they are used") @@ -59,8 +60,8 @@ internal constructor( private val grpcClint: DataConnectGrpcClient by lazy { logger.debug { "DataConnectGrpcClient initialization started" } lock.write { - if (terminated) { - throw IllegalStateException("instance has been terminated") + if (closed) { + throw IllegalStateException("instance has been closed") } settingsFrozen = true @@ -89,12 +90,15 @@ internal constructor( variables: Map ): Struct = grpcClint.executeMutation(revision, operationName, variables) - fun terminate() { - logger.debug { "terminate() called" } + override fun close() { + logger.debug { "close() called" } lock.write { - grpcClint.close() - terminated = true - creator.remove(this) + try { + grpcClint.close() + } finally { + closed = true + creator.remove(this) + } } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt index 5a3a5bfe180..ae826a31a9a 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt @@ -90,7 +90,7 @@ internal class FirebaseDataConnectFactory( instances.addAll(instancesByCacheKey.values) } - instances.forEach { instance -> instance.terminate() } + instances.forEach { instance -> instance.close() } lock.withLock { if (instancesByCacheKey.isNotEmpty()) { From ad410760e1776785438d7193d2046b63e756b458 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Sat, 21 Oct 2023 01:32:00 -0400 Subject: [PATCH 022/573] Logger refactor --- .../dataconnect/FirebaseDataConnectTest.kt | 14 ++++ .../dataconnect/DataConnectGrpcClient.kt | 11 ++- .../dataconnect/FirebaseDataConnect.kt | 10 ++- .../com/google/firebase/dataconnect/Logger.kt | 75 +++++++++++++++---- 4 files changed, 92 insertions(+), 18 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 81462c96543..138947d896d 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -28,6 +28,7 @@ import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock import kotlinx.coroutines.test.runTest import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -43,6 +44,19 @@ class FirebaseDataConnectTest { } } + private lateinit var logLevelBefore: Logger.Level + + @Before + fun setupLogLevel() { + logLevelBefore = logLevel + logLevel = Logger.Level.DEBUG + } + + @After + fun restoreLogLevelBefore() { + logLevel = logLevelBefore + } + @Test fun getInstance_without_specifying_an_app_should_use_the_default_app() { val instance1 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation1", "TestService1") diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index cd8f3fe19ce..8b202bed0c8 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -37,9 +37,14 @@ internal class DataConnectGrpcClient( val service: String, val hostName: String, val port: Int, - val sslEnabled: Boolean + val sslEnabled: Boolean, + creatorLoggerId: String, ) { - private val logger = LoggerImpl("FirebaseDataConnectClient", Logger.Level.DEBUG) + private val logger = Logger("DataConnectGrpcClient") + + init { + logger.debug { "Created from $creatorLoggerId" } + } private val grpcChannel: ManagedChannel by lazy { // Upgrade the Android security provider using Google Play Services. @@ -121,7 +126,9 @@ internal class DataConnectGrpcClient( } fun close() { + logger.debug { "close() starting" } grpcChannel.shutdownNow() + logger.debug { "close() done" } } private fun nameForRevision(revision: String): String = diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index fc59aac2c75..b8d44972f09 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -32,7 +32,14 @@ internal constructor( internal val service: String, private val creator: FirebaseDataConnectFactory ) : Closeable { - private val logger = LoggerImpl("FirebaseDataConnect", Logger.Level.DEBUG) + private val logger = Logger("FirebaseDataConnect") + + init { + logger.debug { + "New instance created with " + + "appName=$appName, projectId=$projectId, location=$location, service=$service" + } + } private val lock = ReentrantReadWriteLock() private var settingsFrozen = false @@ -73,6 +80,7 @@ internal constructor( hostName = settings.hostName, port = settings.port, sslEnabled = settings.sslEnabled, + creatorLoggerId = logger.id, ) .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt index 135a8cb4f9d..ed04f7561e1 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt @@ -14,11 +14,10 @@ package com.google.firebase.dataconnect import android.util.Log +import java.util.concurrent.atomic.AtomicInteger interface Logger { - - val name: String - var level: Level + val id: String fun info(message: () -> Any?) fun debug(message: () -> Any?) @@ -32,23 +31,69 @@ interface Logger { } } -internal class LoggerImpl(override val name: String, override var level: Logger.Level) : Logger { +@Volatile var logLevel: Logger.Level = Logger.Level.INFO - override fun info(message: () -> Any?) { - if (level == Logger.Level.INFO || level == Logger.Level.DEBUG) { - Log.i("FirebaseDataConnect", message().toString()) - } +fun Logger(name: String): Logger = LoggerImpl(name = name, idInt = nextLoggerId.getAndIncrement()) + +private const val LOG_TAG = "FirebaseDataConnect" + +// TODO: Use kotlin.concurrent.AtomicInt once kotlin-stdlib is upgraded to 1.9 +// The initial value is just an arbitrary, non-zero value so that logger IDs are easily searchable +// in logs due to the "uniqueness" of their first 4 digits. +private val nextLoggerId = AtomicInteger(0x591F0000) + +private fun isLogEnabledFor(level: Logger.Level) = + when (logLevel) { + Logger.Level.DEBUG -> + when (level) { + Logger.Level.DEBUG -> true + Logger.Level.INFO -> true + Logger.Level.WARNING -> true + } + Logger.Level.INFO -> + when (level) { + Logger.Level.DEBUG -> false + Logger.Level.INFO -> true + Logger.Level.WARNING -> true + } + Logger.Level.WARNING -> + when (level) { + Logger.Level.DEBUG -> false + Logger.Level.INFO -> false + Logger.Level.WARNING -> true + } } - override fun debug(message: () -> Any?) { - if (level == Logger.Level.DEBUG) { - Log.d("FirebaseDataConnect", message().toString()) - } +private fun runIfLogEnabled(level: Logger.Level, block: () -> Unit) { + if (isLogEnabledFor(level)) { + block() } +} + +private class LoggerImpl(private val name: String, private val idInt: Int) : Logger { + + override val id: String by + lazy(LazyThreadSafetyMode.PUBLICATION) { + StringBuilder().run { + append(name) + append('[') + append("0x") + val idHexString = idInt.toString(16) + repeat(8 - idHexString.length) { append('0') } + append(idHexString) + append(']') + toString() + } + } + + override fun info(message: () -> Any?) = + runIfLogEnabled(Logger.Level.INFO) { Log.i(LOG_TAG, "$id ${message()}") } + + override fun debug(message: () -> Any?) = + runIfLogEnabled(Logger.Level.DEBUG) { Log.d(LOG_TAG, "$id ${message()}") } override fun warn(message: () -> Any?) = warn(null, message) - override fun warn(e: Throwable?, message: () -> Any?) { - Log.w("FirebaseDataConnect", message().toString(), e) - } + override fun warn(e: Throwable?, message: () -> Any?) = + runIfLogEnabled(Logger.Level.WARNING) { Log.w(LOG_TAG, "$id ${message()}", e) } } From 9e8f6b5c9e1612ae56b82b45d93a93696dde452f Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Sat, 21 Oct 2023 01:38:18 -0400 Subject: [PATCH 023/573] FirebaseDataConnectTest.kt: use some more idiomatic kotlin --- .../dataconnect/FirebaseDataConnectTest.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 138947d896d..002921c8eca 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -25,6 +25,7 @@ import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.thread import kotlin.concurrent.withLock import kotlinx.coroutines.test.runTest import org.junit.After @@ -153,9 +154,9 @@ class FirebaseDataConnectTest { val threads = mutableListOf().run { val readyCountDown = AtomicInteger(numThreads) - for (i in 0 until numThreads) { + repeat(numThreads) { i -> add( - Thread { + thread { readyCountDown.decrementAndGet() while (readyCountDown.get() > 0) { /* spin */ @@ -176,14 +177,17 @@ class FirebaseDataConnectTest { toList() } - threads.forEach { it.start() } threads.forEach { it.join() } + // Verify that each thread reported its result. assertThat(createdInstancesByThreadId.size).isEqualTo(8) + + // Choose an arbitrary list of created instances from one of the threads, and use it as the + // "expected" value for all other threads. val expectedInstances = createdInstancesByThreadId.values.toList()[0] assertThat(expectedInstances.size).isEqualTo(15) - createdInstancesByThreadId.keys.forEach { threadId -> - val createdInstances = createdInstancesByThreadId[threadId] + + createdInstancesByThreadId.entries.forEach { (threadId, createdInstances) -> assertWithMessage("instances created by threadId=${threadId}") .that(createdInstances) .containsExactlyElementsIn(expectedInstances) From e2532a160d6f68b0937678270477deeb74c17a01 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Sat, 21 Oct 2023 01:43:56 -0400 Subject: [PATCH 024/573] paramaterize operationSet --- .../dataconnect/FirebaseDataConnectTest.kt | 8 +++++++- .../dataconnect/DataConnectGrpcClient.kt | 10 ++++++---- .../dataconnect/FirebaseDataConnect.kt | 18 ++++++++++++++++-- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 002921c8eca..4b475367558 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -216,12 +216,18 @@ class FirebaseDataConnectTest { dc.executeMutation( revision = "TestRevision", + operationSet = "crud", operationName = "createPost", variables = mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") ) - dc.executeQuery(revision = "TestRevision", operationName = "listPosts", variables = emptyMap()) + dc.executeQuery( + revision = "TestRevision", + operationSet = "crud", + operationName = "listPosts", + variables = emptyMap() + ) } private fun createNonDefaultFirebaseApp(): FirebaseApp { diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 8b202bed0c8..6cbf461defa 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -85,11 +85,12 @@ internal class DataConnectGrpcClient( suspend fun executeQuery( revision: String, + operationSet: String, operationName: String, variables: Map ): Struct { val request = executeQueryRequest { - this.name = nameForRevision(revision) + this.name = name(revision, operationSet) this.operationName = operationName this.variables = structFromMap(variables) } @@ -102,11 +103,12 @@ internal class DataConnectGrpcClient( suspend fun executeMutation( revision: String, + operationSet: String, operationName: String, variables: Map ): Struct { val request = executeMutationRequest { - this.name = nameForRevision(revision) + this.name = name(revision, operationSet) this.operationName = operationName this.variables = struct { this.fields.put("data", value { structValue = structFromMap(variables) }) @@ -131,9 +133,9 @@ internal class DataConnectGrpcClient( logger.debug { "close() done" } } - private fun nameForRevision(revision: String): String = + private fun name(revision: String, operationSet: String): String = "projects/$projectId/locations/$location/services/$service/" + - "operationSets/crud/revisions/$revision" + "operationSets/$operationSet/revisions/$revision" } private fun structFromMap(map: Map): Struct = diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index b8d44972f09..0facac94390 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -88,15 +88,29 @@ internal constructor( suspend fun executeQuery( revision: String, + operationSet: String, operationName: String, variables: Map - ): Struct = grpcClint.executeQuery(revision, operationName, variables) + ): Struct = + grpcClint.executeQuery( + revision = revision, + operationSet = operationSet, + operationName = operationName, + variables = variables + ) suspend fun executeMutation( revision: String, + operationSet: String, operationName: String, variables: Map - ): Struct = grpcClint.executeMutation(revision, operationName, variables) + ): Struct = + grpcClint.executeMutation( + revision = revision, + operationSet = operationSet, + operationName = operationName, + variables = variables + ) override fun close() { logger.debug { "close() called" } From 94df375751dd6a494d2068bfc14aa546c7008679 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Sat, 21 Oct 2023 01:56:37 -0400 Subject: [PATCH 025/573] QueryRef and MutationRef added --- .../dataconnect/FirebaseDataConnectTest.kt | 22 ++++++++------ .../dataconnect/FirebaseDataConnect.kt | 30 +++++++------------ .../firebase/dataconnect/MutationRef.kt | 27 +++++++++++++++++ .../google/firebase/dataconnect/QueryRef.kt | 27 +++++++++++++++++ 4 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 4b475367558..fff18c88751 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -215,18 +215,22 @@ class FirebaseDataConnectTest { dc.settings = dataConnectSettings { connectToEmulator() } dc.executeMutation( - revision = "TestRevision", - operationSet = "crud", - operationName = "createPost", - variables = - mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") + MutationRef( + revision = "TestRevision", + operationSet = "crud", + operationName = "createPost", + variables = + mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") + ) ) dc.executeQuery( - revision = "TestRevision", - operationSet = "crud", - operationName = "listPosts", - variables = emptyMap() + QueryRef( + revision = "TestRevision", + operationSet = "crud", + operationName = "listPosts", + variables = emptyMap() + ) ) } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 0facac94390..81378aa4dae 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -86,30 +86,20 @@ internal constructor( } } - suspend fun executeQuery( - revision: String, - operationSet: String, - operationName: String, - variables: Map - ): Struct = + suspend fun executeQuery(ref: QueryRef): Struct = grpcClint.executeQuery( - revision = revision, - operationSet = operationSet, - operationName = operationName, - variables = variables + revision = ref.revision, + operationSet = ref.operationSet, + operationName = ref.operationName, + variables = ref.variables ) - suspend fun executeMutation( - revision: String, - operationSet: String, - operationName: String, - variables: Map - ): Struct = + suspend fun executeMutation(ref: MutationRef): Struct = grpcClint.executeMutation( - revision = revision, - operationSet = operationSet, - operationName = operationName, - variables = variables + revision = ref.revision, + operationSet = ref.operationSet, + operationName = ref.operationName, + variables = ref.variables ) override fun close() { diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt new file mode 100644 index 00000000000..bae5462377d --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt @@ -0,0 +1,27 @@ +// Copyright 2023 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.dataconnect + +open class MutationRef +constructor( + val revision: String, + val operationSet: String, + val operationName: String, + variables: Map +) { + private val _variables = HashMap(variables) + + val variables: Map + get() = HashMap(_variables) +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt new file mode 100644 index 00000000000..afb187da4cd --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt @@ -0,0 +1,27 @@ +// Copyright 2023 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.dataconnect + +open class QueryRef +constructor( + val revision: String, + val operationSet: String, + val operationName: String, + variables: Map +) { + private val _variables = HashMap(variables) + + val variables: Map + get() = HashMap(_variables) +} From 088d17b2fb4806571b1605617466c80eae33edb5 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 24 Oct 2023 09:53:09 -0400 Subject: [PATCH 026/573] move variables out of QueryRef and MutationRef --- .../dataconnect/FirebaseDataConnectTest.kt | 18 +++++------------- .../dataconnect/FirebaseDataConnect.kt | 8 ++++---- .../google/firebase/dataconnect/MutationRef.kt | 12 +----------- .../google/firebase/dataconnect/QueryRef.kt | 12 +----------- 4 files changed, 11 insertions(+), 39 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index fff18c88751..928e2e13f8d 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -215,22 +215,14 @@ class FirebaseDataConnectTest { dc.settings = dataConnectSettings { connectToEmulator() } dc.executeMutation( - MutationRef( - revision = "TestRevision", - operationSet = "crud", - operationName = "createPost", - variables = - mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") - ) + MutationRef(revision = "TestRevision", operationSet = "crud", operationName = "createPost"), + variables = + mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") ) dc.executeQuery( - QueryRef( - revision = "TestRevision", - operationSet = "crud", - operationName = "listPosts", - variables = emptyMap() - ) + QueryRef(revision = "TestRevision", operationSet = "crud", operationName = "listPosts"), + variables = emptyMap() ) } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 81378aa4dae..b12871e4c14 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -86,20 +86,20 @@ internal constructor( } } - suspend fun executeQuery(ref: QueryRef): Struct = + suspend fun executeQuery(ref: QueryRef, variables: Map): Struct = grpcClint.executeQuery( revision = ref.revision, operationSet = ref.operationSet, operationName = ref.operationName, - variables = ref.variables + variables = variables ) - suspend fun executeMutation(ref: MutationRef): Struct = + suspend fun executeMutation(ref: MutationRef, variables: Map): Struct = grpcClint.executeMutation( revision = ref.revision, operationSet = ref.operationSet, operationName = ref.operationName, - variables = ref.variables + variables = variables ) override fun close() { diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt index bae5462377d..4e1314fe485 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt @@ -14,14 +14,4 @@ package com.google.firebase.dataconnect open class MutationRef -constructor( - val revision: String, - val operationSet: String, - val operationName: String, - variables: Map -) { - private val _variables = HashMap(variables) - - val variables: Map - get() = HashMap(_variables) -} +constructor(val revision: String, val operationSet: String, val operationName: String) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt index afb187da4cd..087f45ba40f 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt @@ -14,14 +14,4 @@ package com.google.firebase.dataconnect open class QueryRef -constructor( - val revision: String, - val operationSet: String, - val operationName: String, - variables: Map -) { - private val _variables = HashMap(variables) - - val variables: Map - get() = HashMap(_variables) -} +constructor(val revision: String, val operationSet: String, val operationName: String) From 90b1a50c24c9dc6299d15acfc8198b237d96648d Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 24 Oct 2023 13:29:43 -0400 Subject: [PATCH 027/573] Move FirebaseDataConnect to an interface and add QuerySubscription --- .../dataconnect/FirebaseDataConnect.kt | 100 +------------- .../dataconnect/FirebaseDataConnectFactory.kt | 3 +- .../dataconnect/FirebaseDataConnectImpl.kt | 127 ++++++++++++++++++ .../firebase/dataconnect/MutationRef.kt | 16 ++- .../google/firebase/dataconnect/QueryRef.kt | 16 ++- .../firebase/dataconnect/QuerySubscription.kt | 116 ++++++++++++++++ .../dataconnect/QuerySubscriptionImpl.kt | 96 +++++++++++++ 7 files changed, 377 insertions(+), 97 deletions(-) create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectImpl.kt create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscriptionImpl.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index b12871e4c14..1ce871d2640 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -13,111 +13,23 @@ // limitations under the License. package com.google.firebase.dataconnect -import android.content.Context import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.protobuf.Struct import java.io.Closeable -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.read -import kotlin.concurrent.write -class FirebaseDataConnect -internal constructor( - private val context: Context, - private val appName: String, - internal val projectId: String, - internal val location: String, - internal val service: String, - private val creator: FirebaseDataConnectFactory -) : Closeable { - private val logger = Logger("FirebaseDataConnect") +interface FirebaseDataConnect : Closeable { - init { - logger.debug { - "New instance created with " + - "appName=$appName, projectId=$projectId, location=$location, service=$service" - } - } - - private val lock = ReentrantReadWriteLock() - private var settingsFrozen = false - private var closed = false - - var settings: FirebaseDataConnectSettings = FirebaseDataConnectSettings.defaultInstance - get() { - lock.read { - return field - } - } - set(value) { - lock.write { - if (closed) { - throw IllegalStateException("instance has been closed") - } - if (settingsFrozen) { - throw IllegalStateException("settings cannot be modified after they are used") - } - field = value - } - logger.debug { "Settings changed to $value" } - } - - private val grpcClint: DataConnectGrpcClient by lazy { - logger.debug { "DataConnectGrpcClient initialization started" } - lock.write { - if (closed) { - throw IllegalStateException("instance has been closed") - } - settingsFrozen = true + var settings: FirebaseDataConnectSettings - DataConnectGrpcClient( - context = context, - projectId = projectId, - location = location, - service = service, - hostName = settings.hostName, - port = settings.port, - sslEnabled = settings.sslEnabled, - creatorLoggerId = logger.id, - ) - .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } - } - } - - suspend fun executeQuery(ref: QueryRef, variables: Map): Struct = - grpcClint.executeQuery( - revision = ref.revision, - operationSet = ref.operationSet, - operationName = ref.operationName, - variables = variables - ) + suspend fun executeQuery(ref: QueryRef, variables: Map): Struct - suspend fun executeMutation(ref: MutationRef, variables: Map): Struct = - grpcClint.executeMutation( - revision = ref.revision, - operationSet = ref.operationSet, - operationName = ref.operationName, - variables = variables - ) + suspend fun executeMutation(ref: MutationRef, variables: Map): Struct - override fun close() { - logger.debug { "close() called" } - lock.write { - try { - grpcClint.close() - } finally { - closed = true - creator.remove(this) - } - } - } + fun subscribeQuery(ref: QueryRef, variables: Map): QuerySubscription - override fun toString(): String { - return "FirebaseDataConnect" + - "{appName=$appName, projectId=$projectId, location=$location, service=$service}" - } + override fun close() companion object { fun getInstance(location: String, service: String): FirebaseDataConnect = diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt index ae826a31a9a..2769d08028f 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt @@ -54,12 +54,13 @@ internal class FirebaseDataConnectFactory( val projectId = firebaseApp.options.projectId ?: "" val newInstance = - FirebaseDataConnect( + FirebaseDataConnectImpl( context = context, appName = firebaseApp.name, projectId = projectId, location = location, service = service, + backgroundDispatcher = backgroundDispatcher, creator = this ) instancesByCacheKey[key] = newInstance diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectImpl.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectImpl.kt new file mode 100644 index 00000000000..4aaed0a0481 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectImpl.kt @@ -0,0 +1,127 @@ +// Copyright 2023 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.dataconnect + +import android.content.Context +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope + +internal class FirebaseDataConnectImpl( + private val context: Context, + private val appName: String, + internal val projectId: String, + internal val location: String, + internal val service: String, + private val backgroundDispatcher: CoroutineDispatcher, + private val creator: FirebaseDataConnectFactory +) : FirebaseDataConnect { + + private val logger = Logger("FirebaseDataConnect") + + init { + logger.debug { + "New instance created with " + + "appName=$appName, projectId=$projectId, location=$location, service=$service" + } + } + + val coroutineScope = CoroutineScope(backgroundDispatcher) + + private val lock = ReentrantReadWriteLock() + private var settingsFrozen = false + private var closed = false + + override var settings: FirebaseDataConnectSettings = FirebaseDataConnectSettings.defaultInstance + get() { + lock.read { + return field + } + } + set(value) { + lock.write { + if (closed) { + throw IllegalStateException("instance has been closed") + } + if (settingsFrozen) { + throw IllegalStateException("settings cannot be modified after they are used") + } + field = value + } + logger.debug { "Settings changed to $value" } + } + + private val grpcClint: DataConnectGrpcClient by lazy { + logger.debug { "DataConnectGrpcClient initialization started" } + lock.write { + if (closed) { + throw IllegalStateException("instance has been closed") + } + settingsFrozen = true + + DataConnectGrpcClient( + context = context, + projectId = projectId, + location = location, + service = service, + hostName = settings.hostName, + port = settings.port, + sslEnabled = settings.sslEnabled, + creatorLoggerId = logger.id, + ) + .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } + } + } + + override suspend fun executeQuery(ref: QueryRef, variables: Map) = + grpcClint.executeQuery( + revision = ref.revision, + operationSet = ref.operationSet, + operationName = ref.operationName, + variables = variables + ) + + override fun subscribeQuery(ref: QueryRef, variables: Map) = + QuerySubscriptionImpl(this, ref, variables) + + override suspend fun executeMutation(ref: MutationRef, variables: Map) = + grpcClint.executeMutation( + revision = ref.revision, + operationSet = ref.operationSet, + operationName = ref.operationName, + variables = variables + ) + + override fun close() { + logger.debug { "close() called" } + lock.write { + coroutineScope.cancel() + try { + grpcClint.close() + } finally { + closed = true + creator.remove(this) + } + } + } + + override fun toString(): String { + return "FirebaseDataConnect" + + "{appName=$appName, projectId=$projectId, location=$location, service=$service}" + } +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt index 4e1314fe485..cec452dd3da 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt @@ -13,5 +13,19 @@ // limitations under the License. package com.google.firebase.dataconnect +import java.util.Objects + open class MutationRef -constructor(val revision: String, val operationSet: String, val operationName: String) +constructor(val revision: String, val operationSet: String, val operationName: String) { + + override fun equals(other: Any?) = + other is MutationRef && + revision == other.revision && + operationSet == other.operationSet && + operationName == other.operationName + + override fun hashCode() = Objects.hash(revision, operationSet, operationName) + + override fun toString() = + "MutationRef{revision=$revision, operationSet=$operationSet operationName=$operationName}" +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt index 087f45ba40f..e71eb88b1f6 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt @@ -13,5 +13,19 @@ // limitations under the License. package com.google.firebase.dataconnect +import java.util.Objects + open class QueryRef -constructor(val revision: String, val operationSet: String, val operationName: String) +constructor(val revision: String, val operationSet: String, val operationName: String) { + + override fun equals(other: Any?) = + other is QueryRef && + revision == other.revision && + operationSet == other.operationSet && + operationName == other.operationName + + override fun hashCode() = Objects.hash(revision, operationSet, operationName) + + override fun toString() = + "QueryRef{revision=$revision, operationSet=$operationSet operationName=$operationName}" +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt new file mode 100644 index 00000000000..f963266df0d --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt @@ -0,0 +1,116 @@ +// Copyright 2023 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.dataconnect + +import com.google.protobuf.Struct +import java.io.Closeable +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel + +/** + * Manages a subscription to a query. + * + * This class is entirely thread-safe. It is completely safe to call any methods, get any property, + * or set any settable property concurrently from multiple threads. + */ +interface QuerySubscription : Closeable { + + /** The `FirebaseDataConnect` instance that this object uses. */ + val dataConnect: FirebaseDataConnect + + /** The query that is executed by this subscription. */ + val query: QueryRef + + /** Returns whether this object has been closed. */ + val isClosed: Boolean + + /** + * Creates and returns a channel to which events from this query subscription are sent. + * + * This property returns a new object each time it is accessed. Each access is equivalent to + * calling [sendTo] with a new [Channel] opened in [Channel.CONFLATED] mode. For greater control + * over the semantics of the channel, call [sendTo] directly. + */ + val channel: ReceiveChannel + + /** + * The variables used when the query is executed. + * + * When this property is set, a _copy_ of the given map is made and stored internally. Therefore, + * any changes to the given map after setting this property have no effect on this object. + * + * Setting this property will cause the underlying query to be re-executed with the new variables, + * as if [reload] had been called. As a result, if the variables are set after this object is + * closed then the query is, in fact, _not_ re-executed (the same as [reload]). + */ + var variables: Map + + /** + * Forcefully re-executes the query and posts the result to all associated channels. + * + * If no channels are registered then this method does nothing and returns as if successful. + * + * If this object is closed, then this method does nothing and returns as if successful. + */ + fun reload() + + /** + * Registers a channel to have events sent to it. + * + * The first invocation of this method triggers actual execution of the query. The result of the + * query execution is sent to each registered channel concurrently. + * + * All subsequent invocations of this method immediately deliver the result most recently sent to + * previously-registered channels. + * + * The given channel will be closed when this object is closed by a call to [close]. If this + * method is invoked _after_ this object is already closed then the given channel will immediately + * be closed. + * + * If the given channel is already registered then it will be registered again and will have each + * event sent to it multiple times, once per registration. That is, this method does _not_ check + * for duplicates. In order to completely unregister a channel, a matching number of + * [stopSendingTo] invocations are required. + */ + fun sendTo(channel: SendChannel) + + /** + * Unregisters a channel from having events sent to it. + * + * If the given channel is not currently registered then this method does nothing and returns as + * if successful. + * + * If this object is closed then this method does nothing and returns as if successful. + */ + fun stopSendingTo(channel: SendChannel) + + /** + * Closes this object. + * + * All channels currently-registered via [sendTo] will be closed by invoking their + * [SendChannel.close] method. + * + * If this object is already closed then this method does nothing. If another thread is + * concurrently invoking this method then this invocation will block, waiting for the other to + * complete the close operation. + */ + override fun close() +} + +sealed interface QueryResult + +class SuccessQueryResult(val data: Struct) : QueryResult + +class FailedQueryResult(val errors: List) : QueryResult diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscriptionImpl.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscriptionImpl.kt new file mode 100644 index 00000000000..f35dae93b01 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscriptionImpl.kt @@ -0,0 +1,96 @@ +// Copyright 2023 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.dataconnect + +import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +internal class QuerySubscriptionImpl( + override val dataConnect: FirebaseDataConnectImpl, + override val query: QueryRef, + initialVariables: Map +) : QuerySubscription { + + private var _variables = AtomicReference(initialVariables.toMap()) + override var variables: Map + get() = _variables.get() + set(value) { + _variables.set(value.toMap()) + reload() + } + + private val registeredChannels = mutableListOf>() + private val jobQueue = Channel(Channel.UNLIMITED) + private val job = + dataConnect.coroutineScope + .launch { + for (workItem in jobQueue) { + when (workItem) { + is RegisterChannelWorkItem -> registeredChannels.add(workItem.channel) + is UnregisterChannelWorkItem -> + for (i in 0 until registeredChannels.size) { + if (registeredChannels[i] === workItem.channel) { + registeredChannels.removeAt(i) + break + } + } + is ReloadWorkItem -> { + val result = dataConnect.executeQuery(query, variables) + registeredChannels.forEach { launch { it.send(SuccessQueryResult(result)) } } + } + } + } + } + .apply { + invokeOnCompletion { + registeredChannels.forEach { it.close() } + registeredChannels.clear() + } + } + + override val isClosed: Boolean + get() = !job.isActive + + override val channel: ReceiveChannel + get() = Channel(Channel.CONFLATED).also { sendTo(it) } + + override fun reload() { + jobQueue.trySend(ReloadWorkItem()) + } + + override fun sendTo(channel: SendChannel) { + jobQueue.trySend(RegisterChannelWorkItem(channel)) + } + + override fun stopSendingTo(channel: SendChannel) { + jobQueue.trySend(UnregisterChannelWorkItem(channel)) + } + + override fun close() { + jobQueue.close() + job.cancel() + } +} + +private sealed interface WorkItem + +private class RegisterChannelWorkItem(val channel: SendChannel) : WorkItem + +private class UnregisterChannelWorkItem(val channel: SendChannel) : WorkItem + +private class ReloadWorkItem : WorkItem From b186b747c3e0decd0c9269f4cfbc0296765e1ddd Mon Sep 17 00:00:00 2001 From: cherylEnkidu <96084918+cherylEnkidu@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:49:30 -0400 Subject: [PATCH 028/573] Drafting Kotlin API Design (#503) * Add flow in core API and design query API for generated SDK * Add reload() * use Channel for Observer Pattern * more channel * Add more api to QueryRef * patch --- .../dataconnect/FirebaseDataConnectTest.kt | 68 +++++++++++++++++++ .../dataconnect/GeneratedSDK/queries.kt | 28 ++++++++ .../firebase/dataconnect/QueryRefBasic.kt | 58 ++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 928e2e13f8d..0727734f611 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -21,6 +21,9 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.firebase.initialize +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicInteger @@ -226,6 +229,71 @@ class FirebaseDataConnectTest { ) } + @Test + fun testReloadWorks() { + runBlocking { + val testingStr = "reload" + val query = TestBasicQuery() + query.testResult = testingStr + + launch { + assertThat(query.listen().receive()).isEqualTo(testingStr) + } + launch { + query.reload() + } + } + } + + @Test + fun testCanAddMultipleListeners() { + runBlocking { + val testingStr = "add multiple listeners" + val query = TestBasicQuery() + query.testResult = testingStr + + launch { + assertThat(query.listen().receive()).isEqualTo(testingStr) + } + launch { + assertThat(query.listen().receive()).isEqualTo(testingStr) + } + launch { + assertThat(query.listeners.size).isEqualTo(2) + query.reload() + } + } + } + + @Test + fun testCancelWorks() { + runBlocking { + val testingTwo = "when there are two listeners" + val testingOne = "when there is one listeners" + val query = TestBasicQuery() + query.testResult = testingTwo + + launch { + val channel = query.listen() + assertThat(channel.receive()).isEqualTo(testingTwo) + query.remove(channel) + } + launch { + val channel = query.listen() + assertThat(channel.receive()).isEqualTo(testingTwo) + assertThat(channel.receive()).isEqualTo(testingOne) + } + launch { + assertThat(query.listeners.size).isEqualTo(2) + query.reload() + delay(2000) + assertThat(query.listeners.size).isEqualTo(1) + query.testResult = testingOne + query.reload() + } + } + } + private fun createNonDefaultFirebaseApp(): FirebaseApp { val firebaseApp = Firebase.initialize( diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt new file mode 100644 index 00000000000..c2abe246042 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt @@ -0,0 +1,28 @@ +package com.google.firebase.dataconnect + +class GetPost(id : String) : QueryRefBasic(mapOf(id to null)) { + fun update(newId : String) { + super.updateVariables(mapOf(newId to null)) + } +} + +data class GetPostResponse( + val content : String, + val comments : List +) { + data class Comment( + val id : Int, + val content : String + ) +} + +class ListPosts : QueryRefBasic>() + +data class ListPostsResponse( + val id : String, + val content : String +) + +class ListPostsOnlyId : QueryRefBasic>() + +data class ListPostsOnlyIdResponse(val id : String) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt new file mode 100644 index 00000000000..d285f07a930 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt @@ -0,0 +1,58 @@ +package com.google.firebase.dataconnect + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel + +abstract class QueryRefBasic constructor(private var variables : Map? = null) { + // TODO: Make private in public API + val listeners = mutableListOf>() + + // TODO: Remove in public API + var testResult : T = "placeholder" as T + + // use suspend to work with grpc call + suspend fun get() : T { + return testResult + } + + protected fun updateVariables(newVariables : Map) { + variables = newVariables + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun listen() : Channel { + val channel = Channel() + listeners.add(channel) + // invoked once the channel is closed or the receiving side of this channel is cancel. + channel.invokeOnClose { listeners.remove(channel) } + return channel + } + + suspend fun reload() { + pushNotifications(get()) + } + + // The reason why not cancel the channel is the developer can reuse it for other things + fun remove(listener : Channel) { + listeners.remove(listener) + } + + fun removeAllListeners() { + listeners.clear() + } + + private suspend fun realtimeStreaming() { + while (true) { + //TODO: wait for backend changes, another channel talks to backend? + pushNotifications("realtime" as T) + } + } + + private suspend fun pushNotifications(value : T) { + for (receiver in listeners) { + receiver.send(value) + } + } +} + +class TestBasicQuery : QueryRefBasic() From b3916634149727ad5f31ad5723b35b79bdeb6cbd Mon Sep 17 00:00:00 2001 From: cherylEnkidu <96084918+cherylEnkidu@users.noreply.github.com> Date: Wed, 25 Oct 2023 16:41:27 -0400 Subject: [PATCH 029/573] Add code sample (#506) --- .../dataconnect/FirebaseDataConnectTest.kt | 25 +++++++ .../dataconnect/GeneratedSDK/queries.kt | 21 +----- .../dataconnect/GeneratedSDK/queries2.kt | 69 +++++++++++++++++++ .../dataconnect/GeneratedSDK/queryResponse.kt | 28 ++++++++ 4 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 0727734f611..95c9ba59704 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -229,6 +229,31 @@ class FirebaseDataConnectTest { ) } + @Test + fun testNewQueryInterface() { + val dc = FirebaseDataConnect.getInstance("TestLocation", "TestService") + dc.settings = dataConnectSettings { connectToEmulator() } + + runBlocking { + val query = ListPostsQuery() + + // one time fetch + launch { + query.get() + } + + // listen to realtime update + launch { + val listener = query.listen() + listener.channel.receive() + } + + launch { + query.listen().reload() + } + } + } + @Test fun testReloadWorks() { runBlocking { diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt index c2abe246042..57a15f2d0d4 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt @@ -6,23 +6,6 @@ class GetPost(id : String) : QueryRefBasic(mapOf(id to null)) { } } -data class GetPostResponse( - val content : String, - val comments : List -) { - data class Comment( - val id : Int, - val content : String - ) -} - -class ListPosts : QueryRefBasic>() - -data class ListPostsResponse( - val id : String, - val content : String -) - -class ListPostsOnlyId : QueryRefBasic>() +class ListPosts : QueryRefBasic() -data class ListPostsOnlyIdResponse(val id : String) +class ListPostsOnlyId : QueryRefBasic() diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt new file mode 100644 index 00000000000..ff7f0f46d56 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt @@ -0,0 +1,69 @@ +package com.google.firebase.dataconnect + +import com.google.protobuf.Struct + +class GetPostQuery(private var id : String) { + private val queryRef = QueryRef("revision", "operationSet", "GetPostQuery") + private val dataConnect = FirebaseDataConnect.getInstance("", "") + fun update(newId : String) { + id = newId + } + + suspend fun get() : GetPostResponse { + return decode(dataConnect.executeQuery(queryRef, encode())) + } + + fun listen() : QuerySubscription { + return dataConnect.subscribeQuery(queryRef, encode()) + } + + private fun encode() : Map { + return mapOf(id to null) + } + + private fun decode(response : Struct) : GetPostResponse { + return GetPostResponse("", emptyList()) + } +} + +class ListPostsQuery { + private val queryRef = QueryRef("revision", "operationSet", "ListPostsQuery") + private val dataConnect = FirebaseDataConnect.getInstance("", "") + + suspend fun get() : ListPostsResponse { + return decode(dataConnect.executeQuery(queryRef, encode())) + } + + fun listen() : QuerySubscription { + return dataConnect.subscribeQuery(queryRef, encode()) + } + + private fun encode() : Map { + return emptyMap() + } + + private fun decode(response : Struct) : ListPostsResponse { + return ListPostsResponse(emptyList()) + } +} + +class ListPostsOnlyIdQuery { + private val queryRef = QueryRef("revision", "operationSet", "ListPostsOnlyIdQuery") + private val dataConnect = FirebaseDataConnect.getInstance("", "") + + suspend fun get() : ListPostsOnlyIdResponse { + return decode(dataConnect.executeQuery(queryRef, encode())) + } + + fun listen() : QuerySubscription { + return dataConnect.subscribeQuery(queryRef, encode()) + } + + private fun encode() : Map { + return emptyMap() + } + + private fun decode(response : Struct) : ListPostsOnlyIdResponse { + return ListPostsOnlyIdResponse(emptyList()) + } +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt new file mode 100644 index 00000000000..5ecd223ef78 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt @@ -0,0 +1,28 @@ +package com.google.firebase.dataconnect + +data class GetPostResponse( + val content : String, + val comments : List +) { + data class Comment( + val id : Int, + val content : String + ) +} + +data class ListPostsResponse( + val posts : List +) { + data class Post( + val id : String, + val content : String + ) +} + +data class ListPostsOnlyIdResponse( + val posts : List +) { + data class Post( + val id : String + ) +} From be7628838fc762962f1efa8695cf3b0daf9d1af0 Mon Sep 17 00:00:00 2001 From: cherylEnkidu Date: Wed, 25 Oct 2023 23:26:17 -0400 Subject: [PATCH 030/573] change code samples --- .../dataconnect/FirebaseDataConnectTest.kt | 4 +- .../dataconnect/GeneratedSDK/queries2.kt | 38 ++++++++++++++++--- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 95c9ba59704..5e84f62a2ab 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -245,11 +245,11 @@ class FirebaseDataConnectTest { // listen to realtime update launch { val listener = query.listen() - listener.channel.receive() + listener.receive() } launch { - query.listen().reload() + query.reload() } } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt index ff7f0f46d56..c4fbd3de861 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt @@ -1,10 +1,16 @@ package com.google.firebase.dataconnect import com.google.protobuf.Struct +import kotlinx.coroutines.channels.ReceiveChannel class GetPostQuery(private var id : String) { private val queryRef = QueryRef("revision", "operationSet", "GetPostQuery") private val dataConnect = FirebaseDataConnect.getInstance("", "") + private val subscribeQuery : QuerySubscription + get() { + return dataConnect.subscribeQuery(queryRef, encode()) + } + fun update(newId : String) { id = newId } @@ -13,8 +19,12 @@ class GetPostQuery(private var id : String) { return decode(dataConnect.executeQuery(queryRef, encode())) } - fun listen() : QuerySubscription { - return dataConnect.subscribeQuery(queryRef, encode()) + fun listen() : ReceiveChannel { + return subscribeQuery.channel + } + + fun reload() { + subscribeQuery.reload() } private fun encode() : Map { @@ -29,13 +39,21 @@ class GetPostQuery(private var id : String) { class ListPostsQuery { private val queryRef = QueryRef("revision", "operationSet", "ListPostsQuery") private val dataConnect = FirebaseDataConnect.getInstance("", "") + private val subscribeQuery : QuerySubscription + get() { + return dataConnect.subscribeQuery(queryRef, encode()) + } suspend fun get() : ListPostsResponse { return decode(dataConnect.executeQuery(queryRef, encode())) } - fun listen() : QuerySubscription { - return dataConnect.subscribeQuery(queryRef, encode()) + fun listen() : ReceiveChannel { + return subscribeQuery.channel + } + + fun reload() { + subscribeQuery.reload() } private fun encode() : Map { @@ -50,13 +68,21 @@ class ListPostsQuery { class ListPostsOnlyIdQuery { private val queryRef = QueryRef("revision", "operationSet", "ListPostsOnlyIdQuery") private val dataConnect = FirebaseDataConnect.getInstance("", "") + private val subscribeQuery : QuerySubscription + get() { + return dataConnect.subscribeQuery(queryRef, encode()) + } suspend fun get() : ListPostsOnlyIdResponse { return decode(dataConnect.executeQuery(queryRef, encode())) } - fun listen() : QuerySubscription { - return dataConnect.subscribeQuery(queryRef, encode()) + fun listen() : ReceiveChannel { + return subscribeQuery.channel + } + + fun reload() { + subscribeQuery.reload() } private fun encode() : Map { From 2d1082d2b783a8a3fb9cbf22834a0dc5b98582c5 Mon Sep 17 00:00:00 2001 From: cherylEnkidu <96084918+cherylEnkidu@users.noreply.github.com> Date: Fri, 27 Oct 2023 15:48:47 -0400 Subject: [PATCH 031/573] new API (#508) --- .../dataconnect/FirebaseDataConnectTest.kt | 83 +--------------- .../dataconnect/GeneratedSDK/queries.kt | 26 ++++- .../dataconnect/GeneratedSDK/queries2.kt | 95 ------------------- .../dataconnect/GeneratedSDK/queryRequest.kt | 27 ++++++ .../dataconnect/GeneratedSDK/queryResponse.kt | 63 ++++++++---- .../firebase/dataconnect/QueryRefBasic.kt | 44 +++++---- 6 files changed, 119 insertions(+), 219 deletions(-) delete mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryRequest.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 5e84f62a2ab..d8acdcb886c 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -230,90 +230,17 @@ class FirebaseDataConnectTest { } @Test - fun testNewQueryInterface() { - val dc = FirebaseDataConnect.getInstance("TestLocation", "TestService") - dc.settings = dataConnectSettings { connectToEmulator() } - - runBlocking { - val query = ListPostsQuery() - - // one time fetch - launch { - query.get() - } - - // listen to realtime update - launch { - val listener = query.listen() - listener.receive() - } - - launch { - query.reload() - } - } - } - - @Test - fun testReloadWorks() { + fun testQueryRefBasicWorks() { runBlocking { val testingStr = "reload" - val query = TestBasicQuery() - query.testResult = testingStr + val query = GetPost() launch { - assertThat(query.listen().receive()).isEqualTo(testingStr) - } - launch { - query.reload() - } - } - } - - @Test - fun testCanAddMultipleListeners() { - runBlocking { - val testingStr = "add multiple listeners" - val query = TestBasicQuery() - query.testResult = testingStr + query.get("id") - launch { - assertThat(query.listen().receive()).isEqualTo(testingStr) - } - launch { - assertThat(query.listen().receive()).isEqualTo(testingStr) - } - launch { - assertThat(query.listeners.size).isEqualTo(2) - query.reload() - } - } - } - - @Test - fun testCancelWorks() { - runBlocking { - val testingTwo = "when there are two listeners" - val testingOne = "when there is one listeners" - val query = TestBasicQuery() - query.testResult = testingTwo + val listener = query.listen() + val value = listener.receive() - launch { - val channel = query.listen() - assertThat(channel.receive()).isEqualTo(testingTwo) - query.remove(channel) - } - launch { - val channel = query.listen() - assertThat(channel.receive()).isEqualTo(testingTwo) - assertThat(channel.receive()).isEqualTo(testingOne) - } - launch { - assertThat(query.listeners.size).isEqualTo(2) - query.reload() - delay(2000) - assertThat(query.listeners.size).isEqualTo(1) - query.testResult = testingOne query.reload() } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt index 57a15f2d0d4..fde9f715db7 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt @@ -1,11 +1,27 @@ package com.google.firebase.dataconnect -class GetPost(id : String) : QueryRefBasic(mapOf(id to null)) { - fun update(newId : String) { - super.updateVariables(mapOf(newId to null)) +class GetPost + : QueryRefBasic("", "", "") +{ + lateinit var request : GetPostRequest + suspend fun get(id : String): GetPostResponse { + request = GetPostRequest(id) + return GetPostResponse(super.get(request)) + } + + suspend fun reload() { + super.reload(request) } } -class ListPosts : QueryRefBasic() +class ListPosts : QueryRefBasic("", "", "") { + suspend fun get(): GetPostResponse { + return GetPostResponse(super.get(ListPostsRequest())) + } +} -class ListPostsOnlyId : QueryRefBasic() +class ListPostsOnlyId : QueryRefBasic("", "", "") { + suspend fun get(): GetPostResponse { + return GetPostResponse(super.get(ListPostsOnlyIdRequest())) + } +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt deleted file mode 100644 index c4fbd3de861..00000000000 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries2.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.google.firebase.dataconnect - -import com.google.protobuf.Struct -import kotlinx.coroutines.channels.ReceiveChannel - -class GetPostQuery(private var id : String) { - private val queryRef = QueryRef("revision", "operationSet", "GetPostQuery") - private val dataConnect = FirebaseDataConnect.getInstance("", "") - private val subscribeQuery : QuerySubscription - get() { - return dataConnect.subscribeQuery(queryRef, encode()) - } - - fun update(newId : String) { - id = newId - } - - suspend fun get() : GetPostResponse { - return decode(dataConnect.executeQuery(queryRef, encode())) - } - - fun listen() : ReceiveChannel { - return subscribeQuery.channel - } - - fun reload() { - subscribeQuery.reload() - } - - private fun encode() : Map { - return mapOf(id to null) - } - - private fun decode(response : Struct) : GetPostResponse { - return GetPostResponse("", emptyList()) - } -} - -class ListPostsQuery { - private val queryRef = QueryRef("revision", "operationSet", "ListPostsQuery") - private val dataConnect = FirebaseDataConnect.getInstance("", "") - private val subscribeQuery : QuerySubscription - get() { - return dataConnect.subscribeQuery(queryRef, encode()) - } - - suspend fun get() : ListPostsResponse { - return decode(dataConnect.executeQuery(queryRef, encode())) - } - - fun listen() : ReceiveChannel { - return subscribeQuery.channel - } - - fun reload() { - subscribeQuery.reload() - } - - private fun encode() : Map { - return emptyMap() - } - - private fun decode(response : Struct) : ListPostsResponse { - return ListPostsResponse(emptyList()) - } -} - -class ListPostsOnlyIdQuery { - private val queryRef = QueryRef("revision", "operationSet", "ListPostsOnlyIdQuery") - private val dataConnect = FirebaseDataConnect.getInstance("", "") - private val subscribeQuery : QuerySubscription - get() { - return dataConnect.subscribeQuery(queryRef, encode()) - } - - suspend fun get() : ListPostsOnlyIdResponse { - return decode(dataConnect.executeQuery(queryRef, encode())) - } - - fun listen() : ReceiveChannel { - return subscribeQuery.channel - } - - fun reload() { - subscribeQuery.reload() - } - - private fun encode() : Map { - return emptyMap() - } - - private fun decode(response : Struct) : ListPostsOnlyIdResponse { - return ListPostsOnlyIdResponse(emptyList()) - } -} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryRequest.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryRequest.kt new file mode 100644 index 00000000000..753f494901a --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryRequest.kt @@ -0,0 +1,27 @@ +package com.google.firebase.dataconnect + +interface Request { + fun encode() : Map +} + +class GetPostRequest(var id : String) : Request { + fun update(newId : String) { + id = newId + } + + override fun encode() : Map { + return emptyMap() + } +} + +class ListPostsRequest : Request { + override fun encode() : Map { + return emptyMap() + } +} + +class ListPostsOnlyIdRequest : Request { + override fun encode() : Map { + return emptyMap() + } +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt index 5ecd223ef78..01ffb80df9c 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt @@ -1,28 +1,51 @@ package com.google.firebase.dataconnect -data class GetPostResponse( - val content : String, - val comments : List -) { - data class Comment( - val id : Int, - val content : String - ) +import com.google.protobuf.Struct + +// TODO: restrict Any access scope or replace it +abstract class Response(val response : Struct) { + open fun decode() : Any { + return "null" + } } -data class ListPostsResponse( - val posts : List -) { +class GetPostResponse(response : Struct) : Response(response) { data class Post( - val id : String, - val content : String - ) + val content : String, + val comments : List) { + data class Comment( + val id : Int, + val content : String + ) + } + + override fun decode() : Post { + return Post("", emptyList()) + } } -data class ListPostsOnlyIdResponse( - val posts : List -) { - data class Post( - val id : String - ) +class ListPostsResponse(response : Struct) : Response(response) { + data class Posts( + val posts : List + ) { + data class Post( + val id : String, + val content : String) + } + + override fun decode() : Posts { + return Posts(emptyList()) + } +} + +class ListPostsOnlyIdResponse(response : Struct) : Response(response) { + data class Posts( + val posts : List + ) { + data class Post(val id : String) + } + + override fun decode() : Posts { + return Posts(emptyList()) + } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt index d285f07a930..05e35a31b1d 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt @@ -2,38 +2,42 @@ package com.google.firebase.dataconnect import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel +import com.google.protobuf.Struct +import kotlinx.coroutines.channels.ReceiveChannel -abstract class QueryRefBasic constructor(private var variables : Map? = null) { - // TODO: Make private in public API - val listeners = mutableListOf>() +// QueryResponse cannot be marked as `out` since we need `remove(listener : Channel)` +abstract class QueryRefBasic constructor( + val revision: String, + val operationSet: String, + val operationName: String + ) { - // TODO: Remove in public API - var testResult : T = "placeholder" as T + val dataConnect = FirebaseDataConnect.getInstance("", "") - // use suspend to work with grpc call - suspend fun get() : T { - return testResult - } + val listeners = mutableListOf>() + + val testQuery = QueryRef(revision, operationSet, operationName) - protected fun updateVariables(newVariables : Map) { - variables = newVariables + // use suspend to work with grpc call + suspend fun get(request : QueryRequest) : Struct { + return dataConnect.executeQuery(testQuery, request.encode()) } @OptIn(ExperimentalCoroutinesApi::class) - fun listen() : Channel { - val channel = Channel() + fun listen() : ReceiveChannel { + val channel = Channel() listeners.add(channel) // invoked once the channel is closed or the receiving side of this channel is cancel. channel.invokeOnClose { listeners.remove(channel) } return channel } - suspend fun reload() { - pushNotifications(get()) + suspend fun reload(request : QueryRequest) { + pushNotifications(get(request)) } // The reason why not cancel the channel is the developer can reuse it for other things - fun remove(listener : Channel) { + fun remove(listener : Channel) { listeners.remove(listener) } @@ -44,15 +48,13 @@ abstract class QueryRefBasic constructor(private var variables : Map() From f9ee45098b906101e876167699314755470a3fa6 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 3 Nov 2023 13:52:28 -0400 Subject: [PATCH 032/573] New API proposal, and lots of improvements to the integration tests, like TestDataConnectFactory.kt and EmulatorController.kt --- .../testing_graphql_schemas/person/ops.gql | 36 +++ .../testing_graphql_schemas/person/schema.gql | 19 ++ .../dataconnect/FirebaseDataConnectTest.kt | 248 ++++++++++++----- .../dataconnect/generated/PostsTest.kt | 102 +++++++ .../testutil/DataConnectLogLevelRule.kt | 24 ++ .../testutil/EmulatorController.kt | 120 ++++++++ .../dataconnect/testutil/FactoryTestRule.kt | 40 +++ .../testutil/TestDataConnectFactory.kt | 46 ++++ .../testutil/TestFirebaseAppFactory.kt | 24 ++ .../server/api/emulator_service.proto | 69 +++++ .../google/firebase/dataconnect/BaseRef.kt | 49 ++++ .../dataconnect/DataConnectGrpcClient.kt | 90 ++++-- .../dataconnect/FirebaseDataConnect.kt | 141 +++++++++- .../dataconnect/FirebaseDataConnectFactory.kt | 4 +- .../dataconnect/FirebaseDataConnectImpl.kt | 127 --------- .../FirebaseDataConnectSettings.kt | 27 +- .../dataconnect/GeneratedSDK/queries.kt | 27 -- .../dataconnect/GeneratedSDK/queryRequest.kt | 27 -- .../dataconnect/GeneratedSDK/queryResponse.kt | 51 ---- .../com/google/firebase/dataconnect/Logger.kt | 55 ++-- .../firebase/dataconnect/MutationRef.kt | 30 +- .../google/firebase/dataconnect/QueryRef.kt | 39 ++- .../firebase/dataconnect/QueryRefBasic.kt | 60 ---- .../firebase/dataconnect/QuerySubscription.kt | 257 +++++++++++------- .../dataconnect/QuerySubscriptionImpl.kt | 96 ------- .../apiproposal/QueryApiProposal.kt | 165 +++++++++++ .../generated/CreatePostMutation.kt | 53 ++++ .../dataconnect/generated/GetPostQuery.kt | 104 +++++++ .../FirebaseDataConnectSettingsTest.kt | 133 +++------ 29 files changed, 1512 insertions(+), 751 deletions(-) create mode 100644 firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql create mode 100644 firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/schema.gql create mode 100644 firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt create mode 100644 firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt create mode 100644 firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/EmulatorController.kt create mode 100644 firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt create mode 100644 firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt create mode 100644 firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt create mode 100644 firebase-dataconnect/src/androidTest/proto/firebase/firemat/emulator/server/api/emulator_service.proto create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt delete mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectImpl.kt delete mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt delete mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryRequest.kt delete mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt delete mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt delete mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscriptionImpl.kt create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt create mode 100644 firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql new file mode 100644 index 00000000000..2be15564b25 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql @@ -0,0 +1,36 @@ +# Copyright 2023 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. + +mutation createPerson($data: Person_Data! @pick(fields: ["id", "name", "age"])) @auth(is: PUBLIC) { + person_insert(data: $data) +} + +mutation deletePerson($id: String!) @auth(is: PUBLIC) { + person_delete(id: $id) +} + +query getPerson($id: String!) @auth(is: PUBLIC) { + person(id: $id) { + name + age + } +} + +query getAllPeople @auth(is: PUBLIC) { + people { + id + name + age + } +} diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/schema.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/schema.gql new file mode 100644 index 00000000000..14c1eedccb9 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/schema.gql @@ -0,0 +1,19 @@ +# Copyright 2023 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. + +type Person @table { + id: String! + name: String! + age: Int +} diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index d8acdcb886c..19c642fe5cc 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -20,46 +20,27 @@ import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app -import com.google.firebase.initialize -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule +import com.google.firebase.dataconnect.testutil.TestDataConnectFactory +import com.google.firebase.dataconnect.testutil.TestFirebaseAppFactory +import com.google.firebase.dataconnect.testutil.installEmulatorSchema import java.util.UUID -import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.thread import kotlin.concurrent.withLock +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class FirebaseDataConnectTest { - private val createdFirebaseApps = CopyOnWriteArrayList() - - @After - fun deleteFirebaseApps() { - while (createdFirebaseApps.isNotEmpty()) { - createdFirebaseApps.removeAt(0).delete() - } - } - - private lateinit var logLevelBefore: Logger.Level - - @Before - fun setupLogLevel() { - logLevelBefore = logLevel - logLevel = Logger.Level.DEBUG - } - - @After - fun restoreLogLevelBefore() { - logLevel = logLevelBefore - } + @JvmField @Rule val firebaseAppFactory = TestFirebaseAppFactory() + @JvmField @Rule val dataConnectFactory = TestDataConnectFactory() + @JvmField @Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() @Test fun getInstance_without_specifying_an_app_should_use_the_default_app() { @@ -98,8 +79,8 @@ class FirebaseDataConnectTest { @Test fun getInstance_should_return_distinct_instances_for_distinct_apps() { - val nonDefaultApp1 = createNonDefaultFirebaseApp() - val nonDefaultApp2 = createNonDefaultFirebaseApp() + val nonDefaultApp1 = firebaseAppFactory.newInstance() + val nonDefaultApp2 = firebaseAppFactory.newInstance() val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp1, "TestLocation", "TestService") val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp2, "TestLocation", "TestService") assertThat(instance1).isNotSameInstanceAs(instance2) @@ -107,7 +88,7 @@ class FirebaseDataConnectTest { @Test fun getInstance_should_return_distinct_instances_for_distinct_locations() { - val nonDefaultApp = createNonDefaultFirebaseApp() + val nonDefaultApp = firebaseAppFactory.newInstance() val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService") val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService") assertThat(instance1).isNotSameInstanceAs(instance2) @@ -115,7 +96,7 @@ class FirebaseDataConnectTest { @Test fun getInstance_should_return_distinct_instances_for_distinct_services() { - val nonDefaultApp = createNonDefaultFirebaseApp() + val nonDefaultApp = firebaseAppFactory.newInstance() val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService1") val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService2") assertThat(instance1).isNotSameInstanceAs(instance2) @@ -123,7 +104,7 @@ class FirebaseDataConnectTest { @Test fun getInstance_should_return_a_new_instance_after_the_instance_is_terminated() { - val nonDefaultApp = createNonDefaultFirebaseApp() + val nonDefaultApp = firebaseAppFactory.newInstance() val instance1A = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") val instance2A = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService2") assertThat(instance1A).isNotSameInstanceAs(instance2A) @@ -145,7 +126,7 @@ class FirebaseDataConnectTest { val apps = mutableListOf().run { for (i in 0..4) { - add(createNonDefaultFirebaseApp()) + add(firebaseAppFactory.newInstance()) } toList() } @@ -200,13 +181,13 @@ class FirebaseDataConnectTest { @Test fun toString_should_return_a_string_that_contains_the_required_information() { - val app = createNonDefaultFirebaseApp() + val app = firebaseAppFactory.newInstance() val instance = FirebaseDataConnect.getInstance(app = app, location = "TestLocation", service = "TestService") val toStringResult = instance.toString() - assertThat(toStringResult).containsMatch("appName=${app.name}\\W") + assertThat(toStringResult).containsMatch("app=${app.name}\\W") assertThat(toStringResult).containsMatch("projectId=${app.options.projectId}\\W") assertThat(toStringResult).containsMatch("location=TestLocation\\W") assertThat(toStringResult).containsMatch("service=TestService\\W") @@ -214,46 +195,175 @@ class FirebaseDataConnectTest { @Test fun helloWorld() = runTest { - val dc = FirebaseDataConnect.getInstance("TestLocation", "TestService") - dc.settings = dataConnectSettings { connectToEmulator() } - - dc.executeMutation( - MutationRef(revision = "TestRevision", operationSet = "crud", operationName = "createPost"), - variables = - mapOf("id" to UUID.randomUUID().toString(), "content" to "${System.currentTimeMillis()}") - ) - - dc.executeQuery( - QueryRef(revision = "TestRevision", operationSet = "crud", operationName = "listPosts"), - variables = emptyMap() - ) + val dc = dataConnectFactory.newInstance(service = "local") + + val postId = "${UUID.randomUUID()}" + val postContent = "${System.currentTimeMillis()}" + + run { + val mutation = + IdentityMutationRef( + dataConnect = dc, + operationName = "createPost", + operationSet = "crud", + revision = "TestRevision", + variables = mapOf("data" to mapOf("id" to postId, "content" to postContent)) + ) + val mutationResponse = mutation.execute() + assertWithMessage("mutationResponse") + .that(mutationResponse) + .containsExactlyEntriesIn(mapOf("post_insert" to null)) + } + + run { + val query = + IdentityQueryRef( + dataConnect = dc, + operationName = "getPost", + operationSet = "crud", + revision = "TestRevision", + variables = mapOf("id" to postId) + ) + val queryResult = query.execute() + assertWithMessage("queryResponse") + .that(queryResult) + .containsExactlyEntriesIn( + mapOf("post" to mapOf("content" to postContent, "comments" to emptyList())) + ) + } } @Test - fun testQueryRefBasicWorks() { - runBlocking { - val testingStr = "reload" - val query = GetPost() + fun testInstallEmulatorSchema() { + suspend fun FirebaseDataConnect.createPerson(id: String, name: String, age: Int? = null) = + IdentityMutationRef( + dataConnect = this, + operationName = "createPerson", + operationSet = "ops", + revision = "42", + variables = + mapOf( + "data" to + buildMap { + put("id", id) + put("name", name) + age?.let { put("age", it) } + } + ) + ) + .execute() + + suspend fun FirebaseDataConnect.getPerson(id: String) = + IdentityQueryRef( + dataConnect = this, + operationName = "getPerson", + operationSet = "ops", + revision = "42", + variables = mapOf("id" to id) + ) + .execute() + + suspend fun FirebaseDataConnect.getAllPeople() = + IdentityQueryRef( + dataConnect = this, + operationName = "getAllPeople", + operationSet = "ops", + revision = "42", + variables = emptyMap() + ) + .execute() + + fun Map<*, *>.assertEqualsGetPersonResponse(name: String, age: Double?) { + assertThat(keys).containsExactly("person") + get("person").let { + assertThat(it).isInstanceOf(Map::class.java) + (it as Map<*, *>).let { assertThat(it).containsExactly("name", name, "age", age) } + } + } - launch { - query.get("id") + data class IdNameAgeTuple(val id: Any?, val name: Any?, val age: Any?) + + fun Map<*, *>.assertEqualsGetPeopleResponse(vararg entries: IdNameAgeTuple) { + assertThat(keys).containsExactly("people") + get("people").let { + assertThat(it).isInstanceOf(List::class.java) + val actualPeople = + (it as Iterable<*>).mapIndexed { index, entry -> + assertWithMessage("people[$index]").that(entry).isInstanceOf(Map::class.java) + (entry as Map<*, *>).let { + assertWithMessage("people[$index].keys") + .that(it.keys) + .containsExactly("id", "name", "age") + IdNameAgeTuple(id = it["id"], name = it["name"], age = it["age"]) + } + } + assertThat(actualPeople).containsExactlyElementsIn(entries) + } + } - val listener = query.listen() - val value = listener.receive() + val dataConnect = dataConnectFactory.newInstance() - query.reload() - } + runBlocking { + dataConnect.installEmulatorSchema("testing_graphql_schemas/person") + + dataConnect.createPerson(id = "TestId1", name = "TestName1") + dataConnect.createPerson(id = "TestId2", name = "TestName2", age = 999) + + dataConnect + .getPerson(id = "TestId1") + .assertEqualsGetPersonResponse(name = "TestName1", age = null) + dataConnect + .getPerson(id = "TestId2") + .assertEqualsGetPersonResponse(name = "TestName2", age = 999.0) + + dataConnect + .getAllPeople() + .assertEqualsGetPeopleResponse( + IdNameAgeTuple(id = "TestId1", name = "TestName1", age = null), + IdNameAgeTuple(id = "TestId2", name = "TestName2", age = 999.0), + ) } } +} - private fun createNonDefaultFirebaseApp(): FirebaseApp { - val firebaseApp = - Firebase.initialize( - Firebase.app.applicationContext, - Firebase.app.options, - UUID.randomUUID().toString() - ) - createdFirebaseApps.add(firebaseApp) - return firebaseApp - } +private class IdentityQueryRef( + dataConnect: FirebaseDataConnect, + operationName: String, + operationSet: String, + revision: String, + variables: Map +) : + QueryRef, Map>( + dataConnect = dataConnect, + operationName = operationName, + operationSet = operationSet, + revision = revision, + variables = variables + ) { + override val codec = + object : Codec, Map> { + override fun encodeVariables(variables: Map) = variables + override fun decodeResult(map: Map) = map + } +} + +private class IdentityMutationRef( + dataConnect: FirebaseDataConnect, + operationName: String, + operationSet: String, + revision: String, + variables: Map +) : + MutationRef, Map>( + dataConnect = dataConnect, + operationName = operationName, + operationSet = operationSet, + revision = revision, + variables = variables + ) { + override val codec = + object : Codec, Map> { + override fun encodeVariables(variables: Map) = variables + override fun decodeResult(map: Map) = map + } } diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt new file mode 100644 index 00000000000..0a5f045b855 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt @@ -0,0 +1,102 @@ +// Copyright 2023 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.dataconnect.generated + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertWithMessage +import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule +import com.google.firebase.dataconnect.testutil.TestDataConnectFactory +import java.util.UUID +import kotlin.math.absoluteValue +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.timeout +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(FlowPreview::class) +@RunWith(AndroidJUnit4::class) +class PostsTest { + + @JvmField @Rule val dataConnectFactory = TestDataConnectFactory() + @JvmField @Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + + @Test + fun createPostThenGetPost() { + val dc = dataConnectFactory.newInstance(service = "local") + + val postId = UUID.randomUUID().toString() + val postContent = Random.Default.nextLong().toString(30) + + runBlocking { + dc.mutations.createPost(id = postId, content = postContent).execute() + + val queryResponse = dc.queries.getPost(id = postId).execute() + assertWithMessage("queryResponse") + .that(queryResponse.post) + .isEqualTo(GetPostQuery.Result.Post(content = postContent, comments = emptyList())) + } + } + + @Test + fun subscribe() { + val dc = dataConnectFactory.newInstance(service = "local") + + val postId1 = UUID.randomUUID().toString() + val postContent1 = Random.Default.nextLong().absoluteValue.toString(30) + val postId2 = UUID.randomUUID().toString() + val postContent2 = Random.Default.nextLong().absoluteValue.toString(30) + + runBlocking { + dc.mutations.createPost(id = postId1, content = postContent1).execute() + dc.mutations.createPost(id = postId2, content = postContent2).execute() + + val query = dc.queries.getPost(id = postId1) + assertWithMessage("lastResult 0").that(query.lastResult).isNull() + + val result1 = query.subscribe().timeout(5.seconds).first() + assertWithMessage("result1.isSuccess").that(result1.isSuccess).isTrue() + assertWithMessage("result1.post.content") + .that(result1.getOrThrow().post.content) + .isEqualTo(postContent1) + + assertWithMessage("lastResult 1").that(query.lastResult).isEqualTo(result1) + + val flow2Job = async { query.subscribe().timeout(5.seconds).take(2).toList() } + + query.update(id = postId2) + + val results2 = flow2Job.await() + assertWithMessage("results2.size").that(results2.size).isEqualTo(2) + assertWithMessage("results2[0].isSuccess").that(results2[0].isSuccess).isTrue() + assertWithMessage("results2[1].isSuccess").that(results2[1].isSuccess).isTrue() + assertWithMessage("results2[0].post.content") + .that(results2[0].getOrThrow().post.content) + .isEqualTo(postContent1) + assertWithMessage("results2[1].post.content") + .that(results2[1].getOrThrow().post.content) + .isEqualTo(postContent2) + + assertWithMessage("lastResult 2").that(query.lastResult).isEqualTo(results2[1]) + } + } +} diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt new file mode 100644 index 00000000000..b8ae54d8d81 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt @@ -0,0 +1,24 @@ +package com.google.firebase.dataconnect.testutil + +import com.google.firebase.dataconnect.LogLevel +import com.google.firebase.dataconnect.logLevel +import org.junit.rules.ExternalResource + +/** + * A JUnit test rule that sets the Firebase Data Connect log level to the desired level, then + * restores it upon completion of the test. + */ +class DataConnectLogLevelRule(val logLevelDuringTest: LogLevel? = LogLevel.DEBUG) : + ExternalResource() { + + private lateinit var logLevelBefore: LogLevel + + override fun before() { + logLevelBefore = logLevel + logLevelDuringTest?.also { logLevel = it } + } + + override fun after() { + logLevel = logLevelBefore + } +} diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/EmulatorController.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/EmulatorController.kt new file mode 100644 index 00000000000..e8d6f44a192 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/EmulatorController.kt @@ -0,0 +1,120 @@ +package com.google.firebase.dataconnect.testutil + +import android.content.res.AssetManager +import com.google.firebase.Firebase +import com.google.firebase.app +import com.google.firebase.dataconnect.FirebaseDataConnect +import firemat.emulator.server.api.EmulatorServiceGrpcKt.EmulatorServiceCoroutineStub +import firemat.emulator.server.api.file +import firemat.emulator.server.api.setupSchemaRequest +import firemat.emulator.server.api.source +import io.grpc.ManagedChannelBuilder +import io.grpc.android.AndroidChannelBuilder +import java.io.InputStreamReader +import java.lang.Exception +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.withContext + +suspend fun FirebaseDataConnect.installEmulatorSchema( + schema: String, + operationSets: Map +) { + val (hostName, port) = settings.run { Pair(hostName, port) } + + val grpcChannel = + ManagedChannelBuilder.forAddress(hostName, port).let { + it.usePlaintext() + it.executor(Dispatchers.IO.asExecutor()) + AndroidChannelBuilder.usingBuilder(it).context(Firebase.app.applicationContext).build() + } + + try { + setupSchema( + EmulatorServiceCoroutineStub(grpcChannel), + serviceId = service, + schema = schema, + operationSets = operationSets + ) + } finally { + grpcChannel.shutdown() + } +} + +suspend fun FirebaseDataConnect.installEmulatorSchema(assetDir: String) { + val assets = app.applicationContext.assets + val schemaFileName = "schema.gql" + + val schema = + withContext(Dispatchers.IO) { + assets.open("$assetDir/$schemaFileName").use { inputStream -> + InputStreamReader(inputStream, Charsets.UTF_8).readText() + } + } + + val loadedAssets = + loadAssets(assets, assetDir) { it.endsWith(".gql") && it != schemaFileName } + .flowOn(Dispatchers.IO) + .toList() + + val operationSets = mutableMapOf() + loadedAssets.forEach { + val operationSet = it.fileName.run { substring(0, length - 4) } + operationSets[operationSet] = it.contents + } + + installEmulatorSchema(schema = schema, operationSets = operationSets) +} + +private fun loadAssets(assets: AssetManager, dirPath: String, filter: (String) -> Boolean) = flow { + val fileNames = + assets.list(dirPath) ?: throw NoSuchAssetError("AssetManager.list($dirPath) returned null") + fileNames.filter(filter).forEach { fileName -> + val contents = + assets.open("$dirPath/$fileName").use { inputStream -> + InputStreamReader(inputStream, Charsets.UTF_8).readText() + } + emit(LoadedAsset(fileName = fileName, contents = contents)) + } +} + +private data class LoadedAsset(val fileName: String, val contents: String) + +private class NoSuchAssetError(message: String) : Exception(message) + +private suspend fun setupSchema( + grpcStub: EmulatorServiceCoroutineStub, + serviceId: String, + schema: String, + operationSets: Map +) { + grpcStub.setupSchema( + setupSchemaRequest { + this.serviceId = serviceId + this.schema = source { + this.files.add( + file { + this.path = "schema/schema.gql" + this.content = schema + } + ) + } + operationSets.forEach { (operationSetName, queriesAndMutations) -> + this.operationSets.put( + operationSetName, + source { + this.files.add( + file { + this.path = "schema/queriesAndMutations.gql" + this.content = queriesAndMutations + } + ) + } + ) + } + } + ) +} diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt new file mode 100644 index 00000000000..794617c9101 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt @@ -0,0 +1,40 @@ +package com.google.firebase.dataconnect.testutil + +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import org.junit.rules.ExternalResource + +/** + * A JUnit test rule that creates instances of an object for use during testing, and cleans them + * upon test completion. + */ +abstract class FactoryTestRule(startId: Long) : ExternalResource() { + + private val nextId = AtomicLong(startId) + private val active = AtomicBoolean(false) + private val instances = CopyOnWriteArrayList() + + fun newInstance(params: P? = null): T { + if (!active.get()) { + throw IllegalStateException("newInstance() may only be called during the test's execution") + } + val instance = createInstance(nextId.getAndIncrement().toString(16), params) + instances.add(instance) + return instance + } + + override fun before() { + active.set(true) + } + + override fun after() { + active.set(false) + while (instances.isNotEmpty()) { + destroyInstance(instances.removeLast()) + } + } + + protected abstract fun createInstance(instanceId: String, params: P?): T + protected abstract fun destroyInstance(instance: T) +} diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt new file mode 100644 index 00000000000..322bf4be667 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt @@ -0,0 +1,46 @@ +// Copyright 2023 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.dataconnect.testutil + +import com.google.firebase.dataconnect.FirebaseDataConnect + +/** + * A JUnit test rule that creates instances of [FirebaseDataConnect] for use during testing, and + * closes them upon test completion. + */ +class TestDataConnectFactory : + FactoryTestRule(startId = 0xccdd0000000L) { + + fun newInstance(location: String? = null, service: String? = null): FirebaseDataConnect = + newInstance(Params(location = location, service = service)) + + override fun createInstance(instanceId: String, params: Params?): FirebaseDataConnect { + val instance = + FirebaseDataConnect.getInstance( + location = params?.location ?: "TestLocation$instanceId", + service = params?.service ?: "TestService$instanceId" + ) + + instance.updateSettings { connectToEmulator() } + + return instance + } + + override fun destroyInstance(instance: FirebaseDataConnect) { + instance.close() + } + + data class Params(val location: String? = null, val service: String? = null) +} diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt new file mode 100644 index 00000000000..20712c66bfd --- /dev/null +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt @@ -0,0 +1,24 @@ +package com.google.firebase.dataconnect.testutil + +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.app +import com.google.firebase.initialize + +/** + * A JUnit test rule that creates instances of [FirebaseApp] for use during testing, and closes them + * upon test completion. + */ +class TestFirebaseAppFactory : FactoryTestRule(startId = 0xaabb0000000L) { + + override fun createInstance(instanceId: String, params: Nothing?) = + Firebase.initialize( + Firebase.app.applicationContext, + Firebase.app.options, + "test-app-$instanceId" + ) + + override fun destroyInstance(instance: FirebaseApp) { + instance.delete() + } +} diff --git a/firebase-dataconnect/src/androidTest/proto/firebase/firemat/emulator/server/api/emulator_service.proto b/firebase-dataconnect/src/androidTest/proto/firebase/firemat/emulator/server/api/emulator_service.proto new file mode 100644 index 00000000000..be1df8a720a --- /dev/null +++ b/firebase-dataconnect/src/androidTest/proto/firebase/firemat/emulator/server/api/emulator_service.proto @@ -0,0 +1,69 @@ +// Adapted from http://google3/firebase/firemat/emulator/server/api/emulator_service.proto;rcl=578315096 + +// API protos for the FireMAT Emulator Service. + +syntax = "proto3"; + +package firemat.emulator.server.api; + +import "google/internal/firebase/firematdata/v0/data_service.proto"; +import "google/protobuf/empty.proto"; + +service EmulatorService { + // Surfaces GQL parsing errors. + rpc GetCompileErrors(GetCompileErrorsRequest) returns (GetCompileErrorsResponse) { + } + + // Sets up a lightweight dummy engine instance. + rpc SetupSchema(SetupSchemaRequest) returns (google.protobuf.Empty) { + } +} + +message GetCompileErrorsRequest { + // Relative path to document being compiled from the firemat.yaml location. + string document_path = 1; + + // This is the `{service}` portion of the schema resource name of + // `projects/{project}/locations/{location}/services/{service}/schemas/{schema}` + // in production APIs. If left empty, this defaults to `local` + string service_id = 2; +} + +message GetCompileErrorsResponse { + repeated google.internal.firebase.firemat.v0.GraphqlError errors = 1; +} + +message SetupSchemaRequest { + // This is the `{service}` portion of the schema resource name of + // `projects/{project}/locations/{location}/services/{service}/schemas/{schema}` + // in production APIs. + string service_id = 1; + + // Each service can have a single schema, named "main". + Source schema = 2; + + // Mapping of operation set name to operation set source. + map operation_sets = 3; +} + +// The below resources should stay in line with the control plane protos found +// here: +// http://google3/google/firebase/dataconnect/v1main/resources.proto;l=395-416;rcl=577941647 + +// Used to represent a set of source files. +message Source { + // The files that comprise the source set. + repeated File files = 1; +} + +// Individual files. +message File { + // The file name including folder path, if applicable. The path should be + // relative to a local workspace (e.g. schema/*.gql) and not an absolute path + // (e.g. /some/absolute/path/config/schema/*.gql). + string path = 1; + + // The file's textual content. There is a 1 MB size limit on the file contents + // (to match server control plane limits). + string content = 2; +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt new file mode 100644 index 00000000000..2d289a30821 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt @@ -0,0 +1,49 @@ +// Copyright 2023 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.dataconnect + +import java.util.concurrent.atomic.AtomicReference + +abstract class BaseRef( + val dataConnect: FirebaseDataConnect, + internal val operationName: String, + internal val operationSet: String, + internal val revision: String, + variables: VariablesType +) { + private val _variables = AtomicReference(variables) + val variables: VariablesType + get() = _variables.get() + + abstract suspend fun execute(): ResultType + + fun update(newVariables: VariablesType) { + _variables.set(newVariables) + onUpdate() + } + + protected open fun onUpdate() {} + + protected interface Codec { + fun encodeVariables(variables: VariablesType): Map + fun decodeResult(map: Map): ResultType + } + + protected abstract val codec: Codec + + internal val variablesAsMap: Map + get() = codec.encodeVariables(variables) + + internal fun resultFromMap(map: Map) = codec.decodeResult(map) +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 6cbf461defa..d6e84aee720 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -18,9 +18,11 @@ import com.google.android.gms.security.ProviderInstaller import com.google.protobuf.NullValue import com.google.protobuf.Struct import com.google.protobuf.Value +import com.google.protobuf.listValue import com.google.protobuf.struct import com.google.protobuf.value import google.internal.firebase.firemat.v0.DataServiceGrpcKt.DataServiceCoroutineStub +import google.internal.firebase.firemat.v0.DataServiceOuterClass.GraphqlError import google.internal.firebase.firemat.v0.executeMutationRequest import google.internal.firebase.firemat.v0.executeQueryRequest import io.grpc.ManagedChannel @@ -83,14 +85,18 @@ internal class DataConnectGrpcClient( private val grpcStub: DataServiceCoroutineStub by lazy { DataServiceCoroutineStub(grpcChannel) } + data class ExecuteQueryResult(val data: Map, val errors: List) + + data class ExecuteMutationResult(val data: Map, val errors: List) + suspend fun executeQuery( - revision: String, operationSet: String, operationName: String, + revision: String, variables: Map - ): Struct { + ): ExecuteQueryResult { val request = executeQueryRequest { - this.name = name(revision, operationSet) + this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName this.variables = structFromMap(variables) } @@ -98,27 +104,31 @@ internal class DataConnectGrpcClient( logger.debug { "executeQuery() sending request: $request" } val response = grpcStub.executeQuery(request) logger.debug { "executeQuery() got response: $response" } - return response.data + return ExecuteQueryResult( + data = mapFromStruct(response.data), + errors = response.errorsList, + ) } suspend fun executeMutation( - revision: String, operationSet: String, operationName: String, + revision: String, variables: Map - ): Struct { + ): ExecuteMutationResult { val request = executeMutationRequest { - this.name = name(revision, operationSet) + this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName - this.variables = struct { - this.fields.put("data", value { structValue = structFromMap(variables) }) - } + this.variables = structFromMap(variables) } logger.debug { "executeMutation() sending request: $request" } val response = grpcStub.executeMutation(request) logger.debug { "executeMutation() got response: $response" } - return response.data + return ExecuteMutationResult( + data = mapFromStruct(response.data), + errors = response.errorsList, + ) } override fun toString(): String { @@ -133,26 +143,50 @@ internal class DataConnectGrpcClient( logger.debug { "close() done" } } - private fun name(revision: String, operationSet: String): String = + private fun name(operationSet: String, revision: String): String = "projects/$projectId/locations/$location/services/$service/" + "operationSets/$operationSet/revisions/$revision" } -private fun structFromMap(map: Map): Struct = - Struct.newBuilder().run { - map.keys.sorted().forEach { key -> putFields(key, protoValueFromObject(map[key])) } - build() +private fun mapFromStruct(struct: Struct): Map = + struct.fieldsMap.mapValues { objectFromStructValue(it.value) } + +private fun objectFromStructValue(struct: Value): Any? = + struct.run { + when (kindCase) { + Value.KindCase.NULL_VALUE -> null + Value.KindCase.BOOL_VALUE -> boolValue + Value.KindCase.NUMBER_VALUE -> numberValue + Value.KindCase.STRING_VALUE -> stringValue + Value.KindCase.LIST_VALUE -> listValue.valuesList.map { objectFromStructValue(it) } + Value.KindCase.STRUCT_VALUE -> mapFromStruct(structValue) + else -> throw IllegalArgumentException("unsupported Struct kind: $kindCase") + } } -private fun protoValueFromObject(obj: Any?): Value = - Value.newBuilder() - .run { - when (obj) { - null -> setNullValue(NullValue.NULL_VALUE) - is String -> setStringValue(obj) - is Boolean -> setBoolValue(obj) - is Double -> setNumberValue(obj) - else -> throw IllegalArgumentException("unsupported value type: ${obj::class}") - } - } - .build() +private fun structFromMap(map: Map) = struct { + map.keys.sorted().forEach { key -> fields.put(key, valueFromObject(map[key])) } +} + +private fun valueFromObject(obj: Any?): Value = value { + when (obj) { + null -> nullValue = NullValue.NULL_VALUE + is String -> stringValue = obj + is Boolean -> boolValue = obj + is Int -> numberValue = obj.toDouble() + is Double -> numberValue = obj + is Map<*, *> -> + structValue = + obj.let { + struct { + it.forEach { entry -> + val key = entry.key as? String ?: error("unsupported map key: $entry.key") + fields.put(key, valueFromObject(entry.value)) + } + } + } + is Iterable<*> -> + listValue = obj.let { listValue { it.forEach { values.add(valueFromObject(it)) } } } + else -> throw IllegalArgumentException("unsupported value type: ${obj::class}") + } +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 1ce871d2640..d6e9e249183 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -13,23 +13,146 @@ // limitations under the License. package com.google.firebase.dataconnect +import android.content.Context import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app -import com.google.protobuf.Struct import java.io.Closeable +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel -interface FirebaseDataConnect : Closeable { +class FirebaseDataConnect +internal constructor( + private val context: Context, + val app: FirebaseApp, + private val projectId: String, + val location: String, + val service: String, + backgroundDispatcher: CoroutineDispatcher, + private val creator: FirebaseDataConnectFactory +) : Closeable { - var settings: FirebaseDataConnectSettings + private val logger = + Logger("FirebaseDataConnect").apply { + debug { + "New instance created with " + + "app=${app.name}, projectId=$projectId, location=$location, service=$service" + } + } - suspend fun executeQuery(ref: QueryRef, variables: Map): Struct + class Queries internal constructor(val dataConnect: FirebaseDataConnect) + val queries = Queries(this) - suspend fun executeMutation(ref: MutationRef, variables: Map): Struct + class Mutations internal constructor(val dataConnect: FirebaseDataConnect) + val mutations = Mutations(this) - fun subscribeQuery(ref: QueryRef, variables: Map): QuerySubscription + internal val coroutineScope = + CoroutineScope( + SupervisorJob() + backgroundDispatcher + CoroutineName("FirebaseDataConnectImpl") + ) - override fun close() + private val lock = ReentrantReadWriteLock() + private var settingsFrozen = false + private var closed = false + + var settings: FirebaseDataConnectSettings = FirebaseDataConnectSettings.defaults + get() { + lock.read { + return field + } + } + set(value) { + lock.write { + if (closed) { + throw IllegalStateException("instance has been closed") + } + if (settingsFrozen) { + throw IllegalStateException("settings cannot be modified after they are used") + } + field = value + } + logger.debug { "Settings changed to $value" } + } + + fun updateSettings(block: FirebaseDataConnectSettings.Builder.() -> Unit) { + settings = settings.builder.build(block) + } + + private val grpcClint: DataConnectGrpcClient by lazy { + logger.debug { "DataConnectGrpcClient initialization started" } + lock.write { + if (closed) { + throw IllegalStateException("instance has been closed") + } + settingsFrozen = true + + DataConnectGrpcClient( + context = context, + projectId = projectId, + location = location, + service = service, + hostName = settings.hostName, + port = settings.port, + sslEnabled = settings.sslEnabled, + creatorLoggerId = logger.id, + ) + .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } + } + } + + internal suspend fun executeQuery(ref: QueryRef): R = + grpcClint + .executeQuery( + operationName = ref.operationName, + operationSet = ref.operationSet, + revision = ref.revision, + variables = ref.variablesAsMap, + ) + .let { response -> + if (response.errors.isNotEmpty()) { + throw RuntimeException("query \"$ref.operationName\" failed: ${response.errors}") + } + ref.resultFromMap(response.data) + } + + internal suspend fun executeMutation(ref: MutationRef): R = + grpcClint + .executeMutation( + operationName = ref.operationName, + operationSet = ref.operationSet, + revision = ref.revision, + variables = ref.variablesAsMap + ) + .let { response -> + if (response.errors.isNotEmpty()) { + throw RuntimeException("mutation \"${ref.operationName}\" failed: ${response.errors}") + } + ref.resultFromMap(response.data) + } + + override fun close() { + logger.debug { "close() called" } + lock.write { + coroutineScope.cancel() + try { + grpcClint.close() + } finally { + closed = true + creator.remove(this) + } + } + } + + override fun toString(): String { + return "FirebaseDataConnect" + + "{app=${app.name}, projectId=$projectId, location=$location, service=$service}" + } companion object { fun getInstance(location: String, service: String): FirebaseDataConnect = @@ -39,3 +162,7 @@ interface FirebaseDataConnect : Closeable { app.get(FirebaseDataConnectFactory::class.java).run { get(location, service) } } } + +open class DataConnectException(message: String) : Exception(message) + +open class ResultDecodeError(message: String) : DataConnectException(message) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt index 2769d08028f..48f77d70ca6 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt @@ -54,9 +54,9 @@ internal class FirebaseDataConnectFactory( val projectId = firebaseApp.options.projectId ?: "" val newInstance = - FirebaseDataConnectImpl( + FirebaseDataConnect( context = context, - appName = firebaseApp.name, + app = firebaseApp, projectId = projectId, location = location, service = service, diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectImpl.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectImpl.kt deleted file mode 100644 index 4aaed0a0481..00000000000 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectImpl.kt +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2023 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.dataconnect - -import android.content.Context -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.read -import kotlin.concurrent.write -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancel -import kotlinx.coroutines.coroutineScope - -internal class FirebaseDataConnectImpl( - private val context: Context, - private val appName: String, - internal val projectId: String, - internal val location: String, - internal val service: String, - private val backgroundDispatcher: CoroutineDispatcher, - private val creator: FirebaseDataConnectFactory -) : FirebaseDataConnect { - - private val logger = Logger("FirebaseDataConnect") - - init { - logger.debug { - "New instance created with " + - "appName=$appName, projectId=$projectId, location=$location, service=$service" - } - } - - val coroutineScope = CoroutineScope(backgroundDispatcher) - - private val lock = ReentrantReadWriteLock() - private var settingsFrozen = false - private var closed = false - - override var settings: FirebaseDataConnectSettings = FirebaseDataConnectSettings.defaultInstance - get() { - lock.read { - return field - } - } - set(value) { - lock.write { - if (closed) { - throw IllegalStateException("instance has been closed") - } - if (settingsFrozen) { - throw IllegalStateException("settings cannot be modified after they are used") - } - field = value - } - logger.debug { "Settings changed to $value" } - } - - private val grpcClint: DataConnectGrpcClient by lazy { - logger.debug { "DataConnectGrpcClient initialization started" } - lock.write { - if (closed) { - throw IllegalStateException("instance has been closed") - } - settingsFrozen = true - - DataConnectGrpcClient( - context = context, - projectId = projectId, - location = location, - service = service, - hostName = settings.hostName, - port = settings.port, - sslEnabled = settings.sslEnabled, - creatorLoggerId = logger.id, - ) - .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } - } - } - - override suspend fun executeQuery(ref: QueryRef, variables: Map) = - grpcClint.executeQuery( - revision = ref.revision, - operationSet = ref.operationSet, - operationName = ref.operationName, - variables = variables - ) - - override fun subscribeQuery(ref: QueryRef, variables: Map) = - QuerySubscriptionImpl(this, ref, variables) - - override suspend fun executeMutation(ref: MutationRef, variables: Map) = - grpcClint.executeMutation( - revision = ref.revision, - operationSet = ref.operationSet, - operationName = ref.operationName, - variables = variables - ) - - override fun close() { - logger.debug { "close() called" } - lock.write { - coroutineScope.cancel() - try { - grpcClint.close() - } finally { - closed = true - creator.remove(this) - } - } - } - - override fun toString(): String { - return "FirebaseDataConnect" + - "{appName=$appName, projectId=$projectId, location=$location, service=$service}" - } -} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt index 4882bf02c6f..15700dff5e4 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt @@ -28,10 +28,14 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin get() = Builder(this) companion object { - val defaultInstance + val defaults get() = FirebaseDataConnectSettings( - SettingsValues(hostName = "firestore.googleapis.com", port = 443, sslEnabled = true) + SettingsValues( + hostName = "firestore.googleapis.com", + port = 443, + sslEnabled = true, + ) ) } @@ -46,10 +50,16 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin sslEnabled = false } - fun build() = + fun build(): FirebaseDataConnectSettings = FirebaseDataConnectSettings( - SettingsValues(hostName = hostName, port = port, sslEnabled = sslEnabled) + SettingsValues( + hostName = hostName, + port = port, + sslEnabled = sslEnabled, + ) ) + + fun build(block: Builder.() -> Unit): FirebaseDataConnectSettings = apply(block).build() } override fun equals(other: Any?) = @@ -64,12 +74,13 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin override fun hashCode() = values.hashCode() override fun toString() = - "FirebaseDataConnectSettings{hostName=$hostName, port=$port, sslEnabled=$sslEnabled}" + "FirebaseDataConnectSettings{" + + "hostName=$hostName, " + + "port=$port, " + + "sslEnabled=$sslEnabled" + + "}" } -inline fun dataConnectSettings(block: FirebaseDataConnectSettings.Builder.() -> Unit) = - FirebaseDataConnectSettings.defaultInstance.builder.apply(block).build() - // Use a data class internally to store the settings to get the convenience of the equals(), // hashCode(), and copy() auto-generated methods. private data class SettingsValues( diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt deleted file mode 100644 index fde9f715db7..00000000000 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queries.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.google.firebase.dataconnect - -class GetPost - : QueryRefBasic("", "", "") -{ - lateinit var request : GetPostRequest - suspend fun get(id : String): GetPostResponse { - request = GetPostRequest(id) - return GetPostResponse(super.get(request)) - } - - suspend fun reload() { - super.reload(request) - } -} - -class ListPosts : QueryRefBasic("", "", "") { - suspend fun get(): GetPostResponse { - return GetPostResponse(super.get(ListPostsRequest())) - } -} - -class ListPostsOnlyId : QueryRefBasic("", "", "") { - suspend fun get(): GetPostResponse { - return GetPostResponse(super.get(ListPostsOnlyIdRequest())) - } -} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryRequest.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryRequest.kt deleted file mode 100644 index 753f494901a..00000000000 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryRequest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.google.firebase.dataconnect - -interface Request { - fun encode() : Map -} - -class GetPostRequest(var id : String) : Request { - fun update(newId : String) { - id = newId - } - - override fun encode() : Map { - return emptyMap() - } -} - -class ListPostsRequest : Request { - override fun encode() : Map { - return emptyMap() - } -} - -class ListPostsOnlyIdRequest : Request { - override fun encode() : Map { - return emptyMap() - } -} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt deleted file mode 100644 index 01ffb80df9c..00000000000 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/GeneratedSDK/queryResponse.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.google.firebase.dataconnect - -import com.google.protobuf.Struct - -// TODO: restrict Any access scope or replace it -abstract class Response(val response : Struct) { - open fun decode() : Any { - return "null" - } -} - -class GetPostResponse(response : Struct) : Response(response) { - data class Post( - val content : String, - val comments : List) { - data class Comment( - val id : Int, - val content : String - ) - } - - override fun decode() : Post { - return Post("", emptyList()) - } -} - -class ListPostsResponse(response : Struct) : Response(response) { - data class Posts( - val posts : List - ) { - data class Post( - val id : String, - val content : String) - } - - override fun decode() : Posts { - return Posts(emptyList()) - } -} - -class ListPostsOnlyIdResponse(response : Struct) : Response(response) { - data class Posts( - val posts : List - ) { - data class Post(val id : String) - } - - override fun decode() : Posts { - return Posts(emptyList()) - } -} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt index ed04f7561e1..e8198d97a40 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt @@ -16,24 +16,25 @@ package com.google.firebase.dataconnect import android.util.Log import java.util.concurrent.atomic.AtomicInteger -interface Logger { +enum class LogLevel { + DEBUG, + INFO, + WARNING, +} + +@Volatile var logLevel: LogLevel = LogLevel.INFO + +internal interface Logger { val id: String fun info(message: () -> Any?) fun debug(message: () -> Any?) fun warn(message: () -> Any?) fun warn(e: Throwable?, message: () -> Any?) - - enum class Level { - DEBUG, - INFO, - WARNING, - } } -@Volatile var logLevel: Logger.Level = Logger.Level.INFO - -fun Logger(name: String): Logger = LoggerImpl(name = name, idInt = nextLoggerId.getAndIncrement()) +internal fun Logger(name: String): Logger = + LoggerImpl(name = name, idInt = nextLoggerId.getAndIncrement()) private const val LOG_TAG = "FirebaseDataConnect" @@ -42,29 +43,29 @@ private const val LOG_TAG = "FirebaseDataConnect" // in logs due to the "uniqueness" of their first 4 digits. private val nextLoggerId = AtomicInteger(0x591F0000) -private fun isLogEnabledFor(level: Logger.Level) = +private fun isLogEnabledFor(level: LogLevel) = when (logLevel) { - Logger.Level.DEBUG -> + LogLevel.DEBUG -> when (level) { - Logger.Level.DEBUG -> true - Logger.Level.INFO -> true - Logger.Level.WARNING -> true + LogLevel.DEBUG -> true + LogLevel.INFO -> true + LogLevel.WARNING -> true } - Logger.Level.INFO -> + LogLevel.INFO -> when (level) { - Logger.Level.DEBUG -> false - Logger.Level.INFO -> true - Logger.Level.WARNING -> true + LogLevel.DEBUG -> false + LogLevel.INFO -> true + LogLevel.WARNING -> true } - Logger.Level.WARNING -> + LogLevel.WARNING -> when (level) { - Logger.Level.DEBUG -> false - Logger.Level.INFO -> false - Logger.Level.WARNING -> true + LogLevel.DEBUG -> false + LogLevel.INFO -> false + LogLevel.WARNING -> true } } -private fun runIfLogEnabled(level: Logger.Level, block: () -> Unit) { +private fun runIfLogEnabled(level: LogLevel, block: () -> Unit) { if (isLogEnabledFor(level)) { block() } @@ -87,13 +88,13 @@ private class LoggerImpl(private val name: String, private val idInt: Int) : Log } override fun info(message: () -> Any?) = - runIfLogEnabled(Logger.Level.INFO) { Log.i(LOG_TAG, "$id ${message()}") } + runIfLogEnabled(LogLevel.INFO) { Log.i(LOG_TAG, "$id ${message()}") } override fun debug(message: () -> Any?) = - runIfLogEnabled(Logger.Level.DEBUG) { Log.d(LOG_TAG, "$id ${message()}") } + runIfLogEnabled(LogLevel.DEBUG) { Log.d(LOG_TAG, "$id ${message()}") } override fun warn(message: () -> Any?) = warn(null, message) override fun warn(e: Throwable?, message: () -> Any?) = - runIfLogEnabled(Logger.Level.WARNING) { Log.w(LOG_TAG, "$id ${message()}", e) } + runIfLogEnabled(LogLevel.WARNING) { Log.w(LOG_TAG, "$id ${message()}", e) } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt index cec452dd3da..9dc4aff9d39 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt @@ -13,19 +13,19 @@ // limitations under the License. package com.google.firebase.dataconnect -import java.util.Objects - -open class MutationRef -constructor(val revision: String, val operationSet: String, val operationName: String) { - - override fun equals(other: Any?) = - other is MutationRef && - revision == other.revision && - operationSet == other.operationSet && - operationName == other.operationName - - override fun hashCode() = Objects.hash(revision, operationSet, operationName) - - override fun toString() = - "MutationRef{revision=$revision, operationSet=$operationSet operationName=$operationName}" +abstract class MutationRef( + dataConnect: FirebaseDataConnect, + operationName: String, + operationSet: String, + revision: String, + variables: VariablesType +) : + BaseRef( + dataConnect = dataConnect, + operationName = operationName, + operationSet = operationSet, + revision = revision, + variables = variables + ) { + override suspend fun execute(): ResultType = dataConnect.executeMutation(this) } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt index e71eb88b1f6..2d5d66ebb5d 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt @@ -13,19 +13,36 @@ // limitations under the License. package com.google.firebase.dataconnect -import java.util.Objects +import kotlinx.coroutines.flow.Flow -open class QueryRef -constructor(val revision: String, val operationSet: String, val operationName: String) { +abstract class QueryRef( + dataConnect: FirebaseDataConnect, + operationName: String, + operationSet: String, + revision: String, + variables: VariablesType +) : + BaseRef( + dataConnect = dataConnect, + operationName = operationName, + operationSet = operationSet, + revision = revision, + variables = variables + ) { + private val subscription: QuerySubscription by lazy { + QuerySubscription(this) + } - override fun equals(other: Any?) = - other is QueryRef && - revision == other.revision && - operationSet == other.operationSet && - operationName == other.operationName + val lastResult: Result? + get() = subscription.lastResult - override fun hashCode() = Objects.hash(revision, operationSet, operationName) + override suspend fun execute(): ResultType = dataConnect.executeQuery(this) - override fun toString() = - "QueryRef{revision=$revision, operationSet=$operationSet operationName=$operationName}" + fun subscribe(): Flow> = subscription.subscribe() + + fun reload(): Unit = subscription.reload() + + override fun onUpdate() { + reload() + } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt deleted file mode 100644 index 05e35a31b1d..00000000000 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRefBasic.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.google.firebase.dataconnect - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.Channel -import com.google.protobuf.Struct -import kotlinx.coroutines.channels.ReceiveChannel - -// QueryResponse cannot be marked as `out` since we need `remove(listener : Channel)` -abstract class QueryRefBasic constructor( - val revision: String, - val operationSet: String, - val operationName: String - ) { - - val dataConnect = FirebaseDataConnect.getInstance("", "") - - val listeners = mutableListOf>() - - val testQuery = QueryRef(revision, operationSet, operationName) - - // use suspend to work with grpc call - suspend fun get(request : QueryRequest) : Struct { - return dataConnect.executeQuery(testQuery, request.encode()) - } - - @OptIn(ExperimentalCoroutinesApi::class) - fun listen() : ReceiveChannel { - val channel = Channel() - listeners.add(channel) - // invoked once the channel is closed or the receiving side of this channel is cancel. - channel.invokeOnClose { listeners.remove(channel) } - return channel - } - - suspend fun reload(request : QueryRequest) { - pushNotifications(get(request)) - } - - // The reason why not cancel the channel is the developer can reuse it for other things - fun remove(listener : Channel) { - listeners.remove(listener) - } - - fun removeAllListeners() { - listeners.clear() - } - - private suspend fun realtimeStreaming() { - while (true) { - //TODO: wait for backend changes, another channel talks to backend? - pushNotifications("realtime" as Struct) - } - } - - private suspend fun pushNotifications(value : Struct) { - for (receiver in listeners) { - receiver.send(value as QueryResponse) - } - } -} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt index f963266df0d..70cfc2f7a24 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt @@ -13,104 +13,173 @@ // limitations under the License. package com.google.firebase.dataconnect -import com.google.protobuf.Struct -import java.io.Closeable +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.job +import kotlinx.coroutines.launch -/** - * Manages a subscription to a query. - * - * This class is entirely thread-safe. It is completely safe to call any methods, get any property, - * or set any settable property concurrently from multiple threads. - */ -interface QuerySubscription : Closeable { - - /** The `FirebaseDataConnect` instance that this object uses. */ - val dataConnect: FirebaseDataConnect - - /** The query that is executed by this subscription. */ - val query: QueryRef - - /** Returns whether this object has been closed. */ - val isClosed: Boolean - - /** - * Creates and returns a channel to which events from this query subscription are sent. - * - * This property returns a new object each time it is accessed. Each access is equivalent to - * calling [sendTo] with a new [Channel] opened in [Channel.CONFLATED] mode. For greater control - * over the semantics of the channel, call [sendTo] directly. - */ - val channel: ReceiveChannel - - /** - * The variables used when the query is executed. - * - * When this property is set, a _copy_ of the given map is made and stored internally. Therefore, - * any changes to the given map after setting this property have no effect on this object. - * - * Setting this property will cause the underlying query to be re-executed with the new variables, - * as if [reload] had been called. As a result, if the variables are set after this object is - * closed then the query is, in fact, _not_ re-executed (the same as [reload]). - */ - var variables: Map - - /** - * Forcefully re-executes the query and posts the result to all associated channels. - * - * If no channels are registered then this method does nothing and returns as if successful. - * - * If this object is closed, then this method does nothing and returns as if successful. - */ - fun reload() - - /** - * Registers a channel to have events sent to it. - * - * The first invocation of this method triggers actual execution of the query. The result of the - * query execution is sent to each registered channel concurrently. - * - * All subsequent invocations of this method immediately deliver the result most recently sent to - * previously-registered channels. - * - * The given channel will be closed when this object is closed by a call to [close]. If this - * method is invoked _after_ this object is already closed then the given channel will immediately - * be closed. - * - * If the given channel is already registered then it will be registered again and will have each - * event sent to it multiple times, once per registration. That is, this method does _not_ check - * for duplicates. In order to completely unregister a channel, a matching number of - * [stopSendingTo] invocations are required. - */ - fun sendTo(channel: SendChannel) - - /** - * Unregisters a channel from having events sent to it. - * - * If the given channel is not currently registered then this method does nothing and returns as - * if successful. - * - * If this object is closed then this method does nothing and returns as if successful. - */ - fun stopSendingTo(channel: SendChannel) - - /** - * Closes this object. - * - * All channels currently-registered via [sendTo] will be closed by invoking their - * [SendChannel.close] method. - * - * If this object is already closed then this method does nothing. If another thread is - * concurrently invoking this method then this invocation will block, waiting for the other to - * complete the close operation. - */ - override fun close() +internal class QuerySubscription +internal constructor(internal val query: QueryRef) { + // TODO: Call `coroutineScope.cancel()` when this object is no longer needed. + internal val coroutineScope = + CoroutineScope( + query.dataConnect.coroutineScope.coroutineContext.let { + it + SupervisorJob(it.job) + CoroutineName("QuerySubscriptionImpl") + } + ) + + private val eventLoop = QuerySubscriptionEventLoop(this) + + val lastResult: Result? + get() = eventLoop.lastResult.get() + + fun reload() = eventLoop.reload() + + fun subscribe() = + channelFlow { + eventLoop.registerSendChannel(channel) + awaitClose { eventLoop.unregisterSendChannel(channel) } + } + .buffer(Channel.UNLIMITED) + + init { + coroutineScope.launch { eventLoop.run() } + } } -sealed interface QueryResult +private class QuerySubscriptionEventLoop( + private val subscription: QuerySubscription +) { + + val lastResult = AtomicReference>() + + private val running = AtomicBoolean(false) + private val eventChannel = Channel(Channel.UNLIMITED) + private val registeredSendChannels = mutableListOf>>() + private var reloadJob: Job? = null + private var pendingReload = false + + suspend fun run() { + if (!running.compareAndSet(false, true)) { + throw IllegalStateException("run() has already been invoked") + } + + try { + for (event in eventChannel) { + processEvent(event) + } + } finally { + registeredSendChannels.clear() + reloadJob = null + } + } + + fun registerSendChannel(channel: SendChannel>) { + eventChannel.trySend(RegisterSendChannelEvent(channel)) + } + + fun unregisterSendChannel(channel: SendChannel>) { + eventChannel.trySend(UnregisterSendChannelEvent(channel)) + } + + fun reload() { + eventChannel.trySend(ReloadEvent) + } + + private fun processEvent(event: Event): Unit = + when (event) { + is RegisterSendChannelEvent -> processRegisterSendChannelEvent(event) + is UnregisterSendChannelEvent -> processUnregisterSendChannelEvent(event) + is ReloadEvent -> processReloadEvent() + is ResultEvent -> processResultEvent(event) + } -class SuccessQueryResult(val data: Struct) : QueryResult + private fun processRegisterSendChannelEvent(event: RegisterSendChannelEvent) { + registeredSendChannels.add(event.typedChannel()) -class FailedQueryResult(val errors: List) : QueryResult + lastResult.get().also { + if (it != null) { + event.typedChannel>().trySend(it) + } else if (reloadJob == null) { + processReloadEvent() + } + } + } + + private fun processUnregisterSendChannelEvent(event: UnregisterSendChannelEvent) { + registeredSendChannels.listIterator().let { + while (it.hasNext()) { + if (it.next() === event.channel) { + it.remove() + break + } + } + } + } + + private fun processReloadEvent() { + if (reloadJob?.isActive == true) { + pendingReload = true + return + } + + reloadJob = + subscription.coroutineScope.launch { + val result = + try { + Result.success(subscription.query.execute()) + } catch (e: Throwable) { + Result.failure(e) + } + + eventChannel.trySend(ResultEvent(result)) + } + } + + private fun processResultEvent(event: ResultEvent) { + if (pendingReload) { + pendingReload = false + reload() + } + + lastResult.set(event.typedResult()) + + registeredSendChannels.iterator().let { + while (it.hasNext()) { + val sendResult = it.next().trySend(event.typedResult()) + if (sendResult.isClosed) { + it.remove() + } + } + } + } + + private sealed interface Event + private object ReloadEvent : Event + + private sealed class SendChannelEvent(val channel: SendChannel<*>) : Event { + @Suppress("UNCHECKED_CAST") + fun typedChannel(): SendChannel { + return channel as SendChannel + } + } + + private class RegisterSendChannelEvent(channel: SendChannel<*>) : SendChannelEvent(channel) + private class UnregisterSendChannelEvent(channel: SendChannel<*>) : SendChannelEvent(channel) + + private class ResultEvent(val result: Result<*>) : Event { + @Suppress("UNCHECKED_CAST") + fun typedResult(): Result { + return result as Result + } + } +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscriptionImpl.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscriptionImpl.kt deleted file mode 100644 index f35dae93b01..00000000000 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscriptionImpl.kt +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2023 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.dataconnect - -import java.util.concurrent.atomic.AtomicReference -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch - -internal class QuerySubscriptionImpl( - override val dataConnect: FirebaseDataConnectImpl, - override val query: QueryRef, - initialVariables: Map -) : QuerySubscription { - - private var _variables = AtomicReference(initialVariables.toMap()) - override var variables: Map - get() = _variables.get() - set(value) { - _variables.set(value.toMap()) - reload() - } - - private val registeredChannels = mutableListOf>() - private val jobQueue = Channel(Channel.UNLIMITED) - private val job = - dataConnect.coroutineScope - .launch { - for (workItem in jobQueue) { - when (workItem) { - is RegisterChannelWorkItem -> registeredChannels.add(workItem.channel) - is UnregisterChannelWorkItem -> - for (i in 0 until registeredChannels.size) { - if (registeredChannels[i] === workItem.channel) { - registeredChannels.removeAt(i) - break - } - } - is ReloadWorkItem -> { - val result = dataConnect.executeQuery(query, variables) - registeredChannels.forEach { launch { it.send(SuccessQueryResult(result)) } } - } - } - } - } - .apply { - invokeOnCompletion { - registeredChannels.forEach { it.close() } - registeredChannels.clear() - } - } - - override val isClosed: Boolean - get() = !job.isActive - - override val channel: ReceiveChannel - get() = Channel(Channel.CONFLATED).also { sendTo(it) } - - override fun reload() { - jobQueue.trySend(ReloadWorkItem()) - } - - override fun sendTo(channel: SendChannel) { - jobQueue.trySend(RegisterChannelWorkItem(channel)) - } - - override fun stopSendingTo(channel: SendChannel) { - jobQueue.trySend(UnregisterChannelWorkItem(channel)) - } - - override fun close() { - jobQueue.close() - job.cancel() - } -} - -private sealed interface WorkItem - -private class RegisterChannelWorkItem(val channel: SendChannel) : WorkItem - -private class UnregisterChannelWorkItem(val channel: SendChannel) : WorkItem - -private class ReloadWorkItem : WorkItem diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt new file mode 100644 index 00000000000..727d5f8867c --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -0,0 +1,165 @@ +package com.google.firebase.dataconnect.apiproposal + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// CORE SDK +//////////////////////////////////////////////////////////////////////////////////////////////////// + +interface FirebaseDataConnect { + + suspend fun executeQuery( + operationName: String, + operationSet: String, + revision: String, + variables: Map + ): Map + + fun subscribe( + operationName: String, + operationSet: String, + revision: String, + variables: Map + ): QuerySubscription + + // NOTE: This interface and val are only needed if the alternative about adding a + // getPost() method by the generated SDK below is accepted. + interface Queries { + val dataConnect: FirebaseDataConnect + } + val queries: Queries +} + +interface QuerySubscription { + fun reload() + fun subscribe(): Flow>> +} + +// Implemented by the generated SDK for each `query` defined in graphql +// Alternative: Delete this interface altogether, because it's only real value is so that +// customers could have a List> if, say, they wanted to reload() every one. +interface QueryRef { + // QUESTION: Should the result of execute() be delivered to the listeners that are subscribed? + // The current implementation does _not_ because it's difficult to know which is the "latest" + // result from concurrent calls to execute(). One workaround would be to serialize the calls to + // execute, but that could introduce unacceptable lag for the invocations at the end of the queue. + suspend fun execute(): T + + fun subscribe(): Flow> + + // Alternative considered: Return `Deferred>` so that customer knows when the reload + // completes. For example, suppose a UI has a "Reload" button and when the customer clicks it they + // get a spinner. The app then awaits the returned "Deferred" object to change the spinner to a + // check mark or red "X". Note that simply waiting for a result to be delivered to a Flow isn't + // sufficient because it's not clear that the result was from the specific call to reload() or + // some previous call to reload() by some other unrelated operation. + fun reload() + + // Alternative considered: add `lastResult`. The problem is, what do we do with this value if the + // variables are changed? Do we clear it? Or do we leave it there even though it came from a + // request with different variables? + val lastResult: Result? +} + +// Alternative considered: Make QueryRef and abstract class +abstract class QueryRefAlt( + // Alternative considered: Make `dataConnect` public for convenience + private val dataConnect: FirebaseDataConnect, + private val operationName: String, + private val operationSet: String, + private val revision: String, + variables: Map +) { + + private val variables = variables.toMap() + private val subscription: QuerySubscription by lazy { + dataConnect.subscribe( + operationName = operationName, + operationSet = operationSet, + revision = revision, + variables = variables + ) + } + + fun reload() = subscription.reload() + + protected suspend fun execute(): T = + parseResult( + dataConnect.executeQuery( + operationName = operationName, + operationSet = operationSet, + revision = revision, + variables = variables + ) + ) + + fun subscribe(): Flow> = subscription.subscribe().map { it.map(::parseResult) } + + protected abstract fun parseResult(map: Map): T +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// GENERATED SDK +//////////////////////////////////////////////////////////////////////////////////////////////////// + +interface GetPostQuery : QueryRef { + data class Variables(val id: String) + + data class Result(val post: Post) { + data class Post(val content: String, val comments: List) { + data class Comment(val id: String, val content: String) + } + } +} + +class GetPostQueryAlt +internal constructor(dataConnect: FirebaseDataConnect, variables: Map) : + QueryRefAlt( + dataConnect = dataConnect, + operationName = "getPost", + operationSet = "crud", + revision = "abc123", + variables = variables + ) { + data class Variables(val id: String) + + data class Result(val post: Post) { + data class Post(val content: String, val comments: List) { + data class Comment(val id: String, val content: String) + } + } + + override fun parseResult(map: Map): Result = TODO() +} + +fun FirebaseDataConnect.query(variables: GetPostQuery.Variables): GetPostQuery = TODO() + +fun demo(dataConnect: FirebaseDataConnect) = runBlocking { + val query = dataConnect.query(GetPostQuery.Variables(id = "abc")) + + println(query.execute()) + + val subscribeJob1 = launch { query.subscribe().collect { println("Got result in job 1: $it") } } + val subscribeJob2 = launch { query.subscribe().collect { println("Got result in job 2: $it") } } + + delay(5000) + query.reload() + delay(5000) + subscribeJob1.cancel() + subscribeJob2.cancel() +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Alternatives Considered +//////////////////////////////////////////////////////////////////////////////////////////////////// + +fun FirebaseDataConnect.Queries.getPost(id: String): GetPostQuery = TODO() + +suspend fun demoAlt(dataConnect: FirebaseDataConnect) { + dataConnect.queries.getPost(id = "abc").execute() +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt new file mode 100644 index 00000000000..498c5d8a894 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt @@ -0,0 +1,53 @@ +// Copyright 2023 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.dataconnect.generated + +import com.google.firebase.dataconnect.FirebaseDataConnect +import com.google.firebase.dataconnect.MutationRef + +class CreatePostMutation(dataConnect: FirebaseDataConnect, variables: Variables) : + MutationRef( + dataConnect = dataConnect, + operationName = "createPost", + operationSet = "crud", + revision = "1234567890abcdef", + variables = variables, + ) { + + data class Variables(val data: PostData) { + data class PostData(val id: String, val content: String) + } + + override val codec = + object : Codec { + override fun encodeVariables(variables: Variables) = + mapOf("data" to variables.data.run { mapOf("id" to id, "content" to content) }) + + override fun decodeResult(map: Map) {} + } +} + +fun FirebaseDataConnect.mutation(variables: CreatePostMutation.Variables): CreatePostMutation = + CreatePostMutation(dataConnect = this, variables = variables) + +fun FirebaseDataConnect.Mutations.createPost(id: String, content: String): CreatePostMutation = + dataConnect.mutation(variablesFor(id = id, content = content)) + +fun CreatePostMutation.update(id: String, content: String): Unit = + update(variablesFor(id = id, content = content)) + +private fun variablesFor(id: String, content: String) = + CreatePostMutation.Variables( + data = CreatePostMutation.Variables.PostData(id = id, content = content) + ) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt new file mode 100644 index 00000000000..a40e8f28267 --- /dev/null +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -0,0 +1,104 @@ +// Copyright 2023 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.dataconnect.generated + +import com.google.firebase.dataconnect.FirebaseDataConnect +import com.google.firebase.dataconnect.QueryRef +import com.google.firebase.dataconnect.ResultDecodeError + +class GetPostQuery(dataConnect: FirebaseDataConnect, variables: Variables) : + QueryRef( + dataConnect = dataConnect, + operationName = "getPost", + operationSet = "crud", + revision = "1234567890abcdef", + variables = variables, + ) { + + data class Variables(val id: String) + + data class Result(val post: Post) { + data class Post(val content: String, val comments: List) { + data class Comment(val id: String, val content: String) + } + } + + override val codec = + object : Codec { + override fun encodeVariables(variables: Variables) = mapOf("id" to variables.id) + + override fun decodeResult(map: Map) = + Result( + map["post"].let { + _decodePost( + data = it as? Map<*, *> ?: decodeError("post", it, "Map", "result", map), + path = "post" + ) + } + ) + + private fun _decodePost(data: Map<*, *>, path: String) = + Result.Post( + content = + data["content"].let { + it as? String ?: decodeError("$path.content", it, "String", path, data) + }, + comments = + data["comments"].let { + _decodeComments( + it as? List<*> ?: decodeError("$path.comments", it, "List", path, data), + "$path.comments" + ) + } + ) + + private fun _decodeComments(data: List<*>, path: String) = + data.mapIndexed { index, it -> + _decodeComment( + it as? Map<*, *> ?: decodeError("$path[$index]", it, "Map", path, data), + "$path[$index]" + ) + } + + private fun _decodeComment(data: Map<*, *>, path: String) = + Result.Post.Comment( + id = + data["id"].let { it as? String ?: decodeError("$path.id", it, "String", path, data) }, + content = + data["content"].let { + it as? String ?: decodeError("$path.content", it, "String", path, data) + } + ) + + private fun decodeError( + path: String, + actual: Any?, + expected: String, + contextName: String, + context: Any? + ): Nothing = + throw ResultDecodeError( + "parsing GetPostQuery.Result failed: \"$path\" was expected to be $expected, " + + "but got $actual ($contextName=$context)" + ) + } +} + +fun FirebaseDataConnect.query(variables: GetPostQuery.Variables): GetPostQuery = + GetPostQuery(dataConnect = this, variables = variables) + +fun FirebaseDataConnect.Queries.getPost(id: String): GetPostQuery = + dataConnect.query(GetPostQuery.Variables(id = id)) + +fun GetPostQuery.update(id: String): Unit = update(GetPostQuery.Variables(id = id)) diff --git a/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt b/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt index f1b10e2c32d..a66240ceb8d 100644 --- a/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt +++ b/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt @@ -23,8 +23,8 @@ import org.robolectric.RobolectricTestRunner class FirebaseDataConnectSettingsTest { @Test - fun `defaultInstance properties should have the expected values`() { - FirebaseDataConnectSettings.defaultInstance.apply { + fun `defaults properties should have the expected values`() { + FirebaseDataConnectSettings.defaults.apply { assertThat(hostName).isEqualTo("firestore.googleapis.com") assertThat(port).isEqualTo(443) assertThat(sslEnabled).isEqualTo(true) @@ -34,11 +34,10 @@ class FirebaseDataConnectSettingsTest { @Test fun `builder should be initialized with values from the object given to the constructor`() { val settings = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = 987654321 sslEnabled = false - build() } settings.builder.apply { assertThat(hostName).isEqualTo("abc123") @@ -49,12 +48,8 @@ class FirebaseDataConnectSettingsTest { @Test fun `builder can change the hostName`() { - val defaultSettings = FirebaseDataConnectSettings.defaultInstance - val settings = - defaultSettings.builder.run { - hostName = "abc123" - build() - } + val defaultSettings = FirebaseDataConnectSettings.defaults + val settings = defaultSettings.builder.build { hostName = "abc123" } settings.apply { assertThat(hostName).isEqualTo("abc123") assertThat(port).isEqualTo(defaultSettings.port) @@ -64,12 +59,8 @@ class FirebaseDataConnectSettingsTest { @Test fun `builder can change the port`() { - val defaultSettings = FirebaseDataConnectSettings.defaultInstance - val settings = - defaultSettings.builder.run { - port = 987654321 - build() - } + val defaultSettings = FirebaseDataConnectSettings.defaults + val settings = defaultSettings.builder.build { port = 987654321 } settings.apply { assertThat(hostName).isEqualTo(defaultSettings.hostName) assertThat(port).isEqualTo(987654321) @@ -79,12 +70,8 @@ class FirebaseDataConnectSettingsTest { @Test fun `builder can change the sslEnabled`() { - val defaultSettings = FirebaseDataConnectSettings.defaultInstance - val settings = - defaultSettings.builder.run { - sslEnabled = !defaultSettings.sslEnabled - build() - } + val defaultSettings = FirebaseDataConnectSettings.defaults + val settings = defaultSettings.builder.build { sslEnabled = !defaultSettings.sslEnabled } settings.apply { assertThat(hostName).isEqualTo(defaultSettings.hostName) assertThat(port).isEqualTo(defaultSettings.port) @@ -94,7 +81,7 @@ class FirebaseDataConnectSettingsTest { @Test fun `connectToEmulator() should set the correct values`() { - FirebaseDataConnectSettings.defaultInstance.builder.apply { + FirebaseDataConnectSettings.defaults.builder.apply { connectToEmulator() assertThat(hostName).isEqualTo("10.0.2.2") assertThat(port).isEqualTo(9510) @@ -105,19 +92,19 @@ class FirebaseDataConnectSettingsTest { @Test @Suppress("ReplaceCallWithBinaryOperator") fun `equals() should return true for same instance`() { - val settings = FirebaseDataConnectSettings.defaultInstance + val settings = FirebaseDataConnectSettings.defaults assertThat(settings.equals(settings)).isTrue() } @Test fun `equals() should return false for null`() { - val settings = FirebaseDataConnectSettings.defaultInstance + val settings = FirebaseDataConnectSettings.defaults assertThat(settings.equals(null)).isFalse() } @Test fun `equals() should return false for a different type`() { - val settings = FirebaseDataConnectSettings.defaultInstance + val settings = FirebaseDataConnectSettings.defaults assertThat(settings.equals("an instance of a different class")).isFalse() } @@ -125,18 +112,16 @@ class FirebaseDataConnectSettingsTest { @Suppress("ReplaceCallWithBinaryOperator") fun `equals() should return true for distinct instances with the same property values`() { val settings1 = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = 987654321 sslEnabled = true - build() } val settings2 = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = 987654321 sslEnabled = true - build() } // validate the assumption that `settings1` and `settings2` are distinct objects. assertThat(settings1).isNotSameInstanceAs(settings2) @@ -147,18 +132,16 @@ class FirebaseDataConnectSettingsTest { @Suppress("ReplaceCallWithBinaryOperator") fun `equals() should return false when hostName differs`() { val settings1 = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = 987654321 sslEnabled = true - build() } val settings2 = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "zzzzzz" port = 987654321 sslEnabled = true - build() } assertThat(settings1.equals(settings2)).isFalse() } @@ -167,18 +150,16 @@ class FirebaseDataConnectSettingsTest { @Suppress("ReplaceCallWithBinaryOperator") fun `equals() should return false when port differs`() { val settings1 = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = 987654321 sslEnabled = true - build() } val settings2 = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = -1 sslEnabled = true - build() } assertThat(settings1.equals(settings2)).isFalse() } @@ -187,43 +168,39 @@ class FirebaseDataConnectSettingsTest { @Suppress("ReplaceCallWithBinaryOperator") fun `equals() should return false when sslEnabled differs`() { val settings1 = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = 987654321 sslEnabled = true - build() } val settings2 = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = 987654321 sslEnabled = false - build() } assertThat(settings1.equals(settings2)).isFalse() } @Test fun `hashCode() should return the same value when invoked on the same object`() { - val settings = FirebaseDataConnectSettings.defaultInstance + val settings = FirebaseDataConnectSettings.defaults assertThat(settings.hashCode()).isEqualTo(settings.hashCode()) } @Test fun `hashCode() should return the same value when invoked on a distinct, but equal object`() { val settings1 = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = 987654321 sslEnabled = true - build() } val settings2 = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = 987654321 sslEnabled = true - build() } // validate the assumption that `settings1` and `settings2` are distinct objects. assertThat(settings1).isNotSameInstanceAs(settings2) @@ -232,84 +209,36 @@ class FirebaseDataConnectSettingsTest { @Test fun `hashCode() should return the different values when hostName differs`() { - val settings1 = - FirebaseDataConnectSettings.defaultInstance.builder.run { - hostName = "abc123" - build() - } - val settings2 = - FirebaseDataConnectSettings.defaultInstance.builder.run { - hostName = "xyz987" - build() - } + val settings1 = FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" } + val settings2 = FirebaseDataConnectSettings.defaults.builder.build { hostName = "xyz987" } assertThat(settings1.hashCode()).isNotEqualTo(settings2.hashCode()) } @Test fun `hashCode() should return the different values when port differs`() { - val settings1 = - FirebaseDataConnectSettings.defaultInstance.builder.run { - port = 987 - build() - } - val settings2 = - FirebaseDataConnectSettings.defaultInstance.builder.run { - port = 123 - build() - } + val settings1 = FirebaseDataConnectSettings.defaults.builder.build { port = 987 } + val settings2 = FirebaseDataConnectSettings.defaults.builder.build { port = 123 } assertThat(settings1.hashCode()).isNotEqualTo(settings2.hashCode()) } @Test fun `hashCode() should return the different values when sslEnabled differs`() { - val settings1 = - FirebaseDataConnectSettings.defaultInstance.builder.run { - sslEnabled = true - build() - } - val settings2 = - FirebaseDataConnectSettings.defaultInstance.builder.run { - sslEnabled = false - build() - } + val settings1 = FirebaseDataConnectSettings.defaults.builder.build { sslEnabled = true } + val settings2 = FirebaseDataConnectSettings.defaults.builder.build { sslEnabled = false } assertThat(settings1.hashCode()).isNotEqualTo(settings2.hashCode()) } @Test fun `toString() should return a string that contains the property values`() { val settings = - FirebaseDataConnectSettings.defaultInstance.builder.run { + FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" port = 987654321 sslEnabled = true - build() } val toStringResult = settings.toString() assertThat(toStringResult).containsMatch("hostName=abc123\\W") assertThat(toStringResult).containsMatch("port=987654321\\W") assertThat(toStringResult).containsMatch("sslEnabled=true\\W") } - - @Test - fun `dataConnectSettings() should use the default values`() { - dataConnectSettings { - assertThat(hostName).isEqualTo(FirebaseDataConnectSettings.defaultInstance.hostName) - assertThat(port).isEqualTo(FirebaseDataConnectSettings.defaultInstance.port) - assertThat(sslEnabled).isEqualTo(FirebaseDataConnectSettings.defaultInstance.sslEnabled) - } - } - - @Test - fun `dataConnectSettings() should create an object that uses the specified values`() { - val settings = dataConnectSettings { - hostName = "abc123" - port = 987654321 - sslEnabled = false - } - settings.apply { - assertThat(hostName).isEqualTo("abc123") - assertThat(port).isEqualTo(987654321) - assertThat(sslEnabled).isEqualTo(false) - } - } } From cfd5461908425e4c3f85259fe2e33fcfe40189be Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 3 Nov 2023 13:53:19 -0400 Subject: [PATCH 033/573] Add dokka support, for generating the kotlin api documentation from source, by running: ../gradlew dokkaHtml --- firebase-dataconnect/firebase-dataconnect.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts index bae5e4041ba..8ec3feac53f 100644 --- a/firebase-dataconnect/firebase-dataconnect.gradle.kts +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -16,6 +16,7 @@ plugins { id("firebase-library") id("kotlin-android") id("com.google.protobuf") + id("org.jetbrains.dokka") version "1.9.10" } firebaseLibrary { From 33b4e7708a657f73a0dd4e60bd3c87896204d090 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 3 Nov 2023 14:14:39 -0400 Subject: [PATCH 034/573] QueryApiProposal.kt updated (#510) --- .../apiproposal/QueryApiProposal.kt | 158 +++++++----------- 1 file changed, 62 insertions(+), 96 deletions(-) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index 727d5f8867c..89b2b0b61a3 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -2,8 +2,6 @@ package com.google.firebase.dataconnect.apiproposal import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -11,46 +9,60 @@ import kotlinx.coroutines.runBlocking // CORE SDK //////////////////////////////////////////////////////////////////////////////////////////////////// -interface FirebaseDataConnect { - - suspend fun executeQuery( - operationName: String, - operationSet: String, - revision: String, - variables: Map - ): Map - - fun subscribe( - operationName: String, - operationSet: String, - revision: String, - variables: Map - ): QuerySubscription +class FirebaseDataConnect { // NOTE: This interface and val are only needed if the alternative about adding a // getPost() method by the generated SDK below is accepted. - interface Queries { - val dataConnect: FirebaseDataConnect - } - val queries: Queries + class Queries internal constructor(val dataConnect: FirebaseDataConnect) + val queries: Queries = TODO() } -interface QuerySubscription { - fun reload() - fun subscribe(): Flow>> +open class DataConnectException(message: String) : Exception(message) +open class ResultDecodeError(message: String) : DataConnectException(message) + +abstract class BaseRef( + val dataConnect: FirebaseDataConnect, + operationName: String, + operationSet: String, + revision: String, + variables: VariablesType +) { + val variables: VariablesType = TODO() + + abstract suspend fun execute(): ResultType + + protected open fun onUpdate() {} + + protected interface Codec { + fun encodeVariables(variables: VariablesType): Map + fun decodeResult(map: Map): ResultType + } + + protected abstract val codec: Codec } -// Implemented by the generated SDK for each `query` defined in graphql -// Alternative: Delete this interface altogether, because it's only real value is so that -// customers could have a List> if, say, they wanted to reload() every one. -interface QueryRef { +abstract class QueryRef( + dataConnect: FirebaseDataConnect, + operationName: String, + operationSet: String, + revision: String, + variables: VariablesType +) : + BaseRef( + dataConnect = dataConnect, + operationName = operationName, + operationSet = operationSet, + revision = revision, + variables = variables + ) { + // QUESTION: Should the result of execute() be delivered to the listeners that are subscribed? // The current implementation does _not_ because it's difficult to know which is the "latest" // result from concurrent calls to execute(). One workaround would be to serialize the calls to // execute, but that could introduce unacceptable lag for the invocations at the end of the queue. - suspend fun execute(): T + override suspend fun execute(): ResultType = TODO() - fun subscribe(): Flow> + fun subscribe(): Flow> = TODO() // Alternative considered: Return `Deferred>` so that customer knows when the reload // completes. For example, suppose a UI has a "Reload" button and when the customer clicks it they @@ -58,74 +70,27 @@ interface QueryRef { // check mark or red "X". Note that simply waiting for a result to be delivered to a Flow isn't // sufficient because it's not clear that the result was from the specific call to reload() or // some previous call to reload() by some other unrelated operation. - fun reload() + fun reload(): Unit = TODO() // Alternative considered: add `lastResult`. The problem is, what do we do with this value if the // variables are changed? Do we clear it? Or do we leave it there even though it came from a // request with different variables? - val lastResult: Result? -} - -// Alternative considered: Make QueryRef and abstract class -abstract class QueryRefAlt( - // Alternative considered: Make `dataConnect` public for convenience - private val dataConnect: FirebaseDataConnect, - private val operationName: String, - private val operationSet: String, - private val revision: String, - variables: Map -) { - - private val variables = variables.toMap() - private val subscription: QuerySubscription by lazy { - dataConnect.subscribe( - operationName = operationName, - operationSet = operationSet, - revision = revision, - variables = variables - ) - } - - fun reload() = subscription.reload() - - protected suspend fun execute(): T = - parseResult( - dataConnect.executeQuery( - operationName = operationName, - operationSet = operationSet, - revision = revision, - variables = variables - ) - ) - - fun subscribe(): Flow> = subscription.subscribe().map { it.map(::parseResult) } - - protected abstract fun parseResult(map: Map): T + val lastResult: Result? get() = TODO() } //////////////////////////////////////////////////////////////////////////////////////////////////// // GENERATED SDK //////////////////////////////////////////////////////////////////////////////////////////////////// -interface GetPostQuery : QueryRef { - data class Variables(val id: String) - - data class Result(val post: Post) { - data class Post(val content: String, val comments: List) { - data class Comment(val id: String, val content: String) - } - } -} - -class GetPostQueryAlt -internal constructor(dataConnect: FirebaseDataConnect, variables: Map) : - QueryRefAlt( +class GetPostQuery(dataConnect: FirebaseDataConnect, variables: Variables) : + QueryRef( dataConnect = dataConnect, operationName = "getPost", operationSet = "crud", - revision = "abc123", - variables = variables + revision = "1234567890abcdef", + variables = variables, ) { + data class Variables(val id: String) data class Result(val post: Post) { @@ -134,10 +99,21 @@ internal constructor(dataConnect: FirebaseDataConnect, variables: Map): Result = TODO() + override val codec = object : Codec { + override fun encodeVariables(variables: Variables) = TODO() + override fun decodeResult(map: Map) = TODO() + } } -fun FirebaseDataConnect.query(variables: GetPostQuery.Variables): GetPostQuery = TODO() +fun FirebaseDataConnect.query(variables: GetPostQuery.Variables): GetPostQuery = + GetPostQuery(dataConnect = this, variables = variables) + +fun FirebaseDataConnect.Queries.getPost(id: String): GetPostQuery = + dataConnect.query(GetPostQuery.Variables(id = id)) + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// CUSTOMER CODE +//////////////////////////////////////////////////////////////////////////////////////////////////// fun demo(dataConnect: FirebaseDataConnect) = runBlocking { val query = dataConnect.query(GetPostQuery.Variables(id = "abc")) @@ -153,13 +129,3 @@ fun demo(dataConnect: FirebaseDataConnect) = runBlocking { subscribeJob1.cancel() subscribeJob2.cancel() } - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// Alternatives Considered -//////////////////////////////////////////////////////////////////////////////////////////////////// - -fun FirebaseDataConnect.Queries.getPost(id: String): GetPostQuery = TODO() - -suspend fun demoAlt(dataConnect: FirebaseDataConnect) { - dataConnect.queries.getPost(id = "abc").execute() -} From 724fba33a57eb81ccaf82b091455edeaaf33c058 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 6 Nov 2023 15:32:54 -0500 Subject: [PATCH 035/573] Query/Mutation execute w/ variables redesign (#511) --- .../testing_graphql_schemas/person/ops.gql | 6 +- .../dataconnect/FirebaseDataConnectTest.kt | 105 +++------- .../dataconnect/QuerySubscriptionTest.kt | 120 +++++++++++ .../dataconnect/generated/PostsTest.kt | 22 +- .../dataconnect/testutil/FactoryTestRule.kt | 12 +- .../dataconnect/testutil/IdentityCodec.kt | 37 ++++ .../testutil/TestDataConnectFactory.kt | 2 +- .../testutil/TestFirebaseAppFactory.kt | 2 +- .../testutil/schemas/PersonSchema.kt | 134 ++++++++++++ .../google/firebase/dataconnect/BaseRef.kt | 36 +--- .../dataconnect/FirebaseDataConnect.kt | 31 ++- .../firebase/dataconnect/MutationRef.kt | 9 +- .../google/firebase/dataconnect/QueryRef.kt | 27 +-- .../firebase/dataconnect/QuerySubscription.kt | 197 +++++------------- .../apiproposal/QueryApiProposal.kt | 167 +++++++++------ .../generated/CreatePostMutation.kt | 23 +- .../dataconnect/generated/GetPostQuery.kt | 137 ++++++------ 17 files changed, 617 insertions(+), 450 deletions(-) create mode 100644 firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt create mode 100644 firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/IdentityCodec.kt create mode 100644 firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql index 2be15564b25..eacad92255d 100644 --- a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql @@ -17,7 +17,11 @@ mutation createPerson($data: Person_Data! @pick(fields: ["id", "name", "age"])) } mutation deletePerson($id: String!) @auth(is: PUBLIC) { - person_delete(id: $id) + person_delete(id: $id) +} + +mutation updatePerson($id: String!, $data: Person_Data! @pick(fields: ["name", "age"])) @auth(is: PUBLIC) { + person_update(id: $id, data: $data) } query getPerson($id: String!) @auth(is: PUBLIC) { diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 19c642fe5cc..61cd61750c0 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -21,6 +21,8 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule +import com.google.firebase.dataconnect.testutil.IdentityMutationRef +import com.google.firebase.dataconnect.testutil.IdentityQueryRef import com.google.firebase.dataconnect.testutil.TestDataConnectFactory import com.google.firebase.dataconnect.testutil.TestFirebaseAppFactory import com.google.firebase.dataconnect.testutil.installEmulatorSchema @@ -206,10 +208,10 @@ class FirebaseDataConnectTest { dataConnect = dc, operationName = "createPost", operationSet = "crud", - revision = "TestRevision", - variables = mapOf("data" to mapOf("id" to postId, "content" to postContent)) + revision = "TestRevision" ) - val mutationResponse = mutation.execute() + val mutationResponse = + mutation.execute(mapOf("data" to mapOf("id" to postId, "content" to postContent))) assertWithMessage("mutationResponse") .that(mutationResponse) .containsExactlyEntriesIn(mapOf("post_insert" to null)) @@ -221,10 +223,9 @@ class FirebaseDataConnectTest { dataConnect = dc, operationName = "getPost", operationSet = "crud", - revision = "TestRevision", - variables = mapOf("id" to postId) + revision = "TestRevision" ) - val queryResult = query.execute() + val queryResult = query.execute(mapOf("id" to postId)) assertWithMessage("queryResponse") .that(queryResult) .containsExactlyEntriesIn( @@ -240,44 +241,42 @@ class FirebaseDataConnectTest { dataConnect = this, operationName = "createPerson", operationSet = "ops", - revision = "42", - variables = - mapOf( - "data" to - buildMap { - put("id", id) - put("name", name) - age?.let { put("age", it) } - } - ) + revision = "42" + ) + .execute( + mapOf( + "data" to + buildMap { + put("id", id) + put("name", name) + age?.let { put("age", it) } + } + ) ) - .execute() suspend fun FirebaseDataConnect.getPerson(id: String) = IdentityQueryRef( dataConnect = this, operationName = "getPerson", operationSet = "ops", - revision = "42", - variables = mapOf("id" to id) + revision = "42" ) - .execute() + .execute(mapOf("id" to id)) suspend fun FirebaseDataConnect.getAllPeople() = IdentityQueryRef( dataConnect = this, operationName = "getAllPeople", operationSet = "ops", - revision = "42", - variables = emptyMap() + revision = "42" ) - .execute() + .execute(emptyMap()) fun Map<*, *>.assertEqualsGetPersonResponse(name: String, age: Double?) { assertThat(keys).containsExactly("person") - get("person").let { - assertThat(it).isInstanceOf(Map::class.java) - (it as Map<*, *>).let { assertThat(it).containsExactly("name", name, "age", age) } + get("person").let { personMap -> + assertThat(personMap).isInstanceOf(Map::class.java) + (personMap as Map<*, *>).let { assertThat(it).containsExactly("name", name, "age", age) } } } @@ -285,16 +284,16 @@ class FirebaseDataConnectTest { fun Map<*, *>.assertEqualsGetPeopleResponse(vararg entries: IdNameAgeTuple) { assertThat(keys).containsExactly("people") - get("people").let { - assertThat(it).isInstanceOf(List::class.java) + get("people").let { peopleList -> + assertThat(peopleList).isInstanceOf(List::class.java) val actualPeople = - (it as Iterable<*>).mapIndexed { index, entry -> + (peopleList as Iterable<*>).mapIndexed { index, entry -> assertWithMessage("people[$index]").that(entry).isInstanceOf(Map::class.java) - (entry as Map<*, *>).let { + (entry as Map<*, *>).let { personMap -> assertWithMessage("people[$index].keys") - .that(it.keys) + .that(personMap.keys) .containsExactly("id", "name", "age") - IdNameAgeTuple(id = it["id"], name = it["name"], age = it["age"]) + IdNameAgeTuple(id = personMap["id"], name = personMap["name"], age = personMap["age"]) } } assertThat(actualPeople).containsExactlyElementsIn(entries) @@ -325,45 +324,3 @@ class FirebaseDataConnectTest { } } } - -private class IdentityQueryRef( - dataConnect: FirebaseDataConnect, - operationName: String, - operationSet: String, - revision: String, - variables: Map -) : - QueryRef, Map>( - dataConnect = dataConnect, - operationName = operationName, - operationSet = operationSet, - revision = revision, - variables = variables - ) { - override val codec = - object : Codec, Map> { - override fun encodeVariables(variables: Map) = variables - override fun decodeResult(map: Map) = map - } -} - -private class IdentityMutationRef( - dataConnect: FirebaseDataConnect, - operationName: String, - operationSet: String, - revision: String, - variables: Map -) : - MutationRef, Map>( - dataConnect = dataConnect, - operationName = operationName, - operationSet = operationSet, - revision = revision, - variables = variables - ) { - override val codec = - object : Codec, Map> { - override fun encodeVariables(variables: Map) = variables - override fun decodeResult(map: Map) = map - } -} diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt new file mode 100644 index 00000000000..37a8a38e8c3 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt @@ -0,0 +1,120 @@ +// Copyright 2023 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. + +@file:OptIn(FlowPreview::class) + +package com.google.firebase.dataconnect + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule +import com.google.firebase.dataconnect.testutil.TestDataConnectFactory +import com.google.firebase.dataconnect.testutil.schemas.* +import java.util.concurrent.Executors +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class QuerySubscriptionTest { + + @JvmField @Rule val dataConnectFactory = TestDataConnectFactory() + @JvmField @Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + + private lateinit var schema: PersonSchema + + @Before + fun initializePersonSchema() { + schema = PersonSchema(dataConnectFactory.newInstance()) + runBlocking { schema.installEmulatorSchema() } + } + + @Test + fun lastResult_should_be_null_on_new_instance() { + val querySubscription = schema.getPerson.subscribe(id = "42") + assertThat(querySubscription.lastResult).isNull() + } + + @Test + fun lastResult_should_be_equal_to_the_last_collected_result() = runBlocking { + schema.createPerson.execute(id = "TestId", name = "TestPerson", age = 42) + val querySubscription = schema.getPerson.subscribe(id = "42") + val result = querySubscription.flow.timeout(2000.seconds).first() + assertThat(querySubscription.lastResult).isEqualTo(result) + } + + @Test + fun flow_collect_should_trigger_reload() = runBlocking { + schema.createPerson.execute(id = "TestId12345", name = "Name0", age = 10000) + val querySubscription = schema.getPerson.subscribe(id = "TestId12345") + + withTimeout(2.seconds) { + val resultsChannel = + Channel(capacity = Channel.UNLIMITED) + val collectJob = launch { + querySubscription.flow.collect { resultsChannel.send(it.getOrThrow()) } + } + + val result1 = resultsChannel.receive() + assertThat(result1).isEqualToGetPersonQueryResult(name = "Name0", age = 10000) + + schema.updatePerson.execute(id = "TestId12345", name = "Name1", age = 10001) + + querySubscription.reload() + val result2 = resultsChannel.receive() + assertThat(result2).isEqualToGetPersonQueryResult(name = "Name1", age = 10001) + + collectJob.cancel() + } + } + + @Test + fun calling_reload_many_times_concurrently_does_not_cause_issues() = runBlocking { + schema.createPerson.execute(id = "TestId12345", name = "Name", age = 10000) + val querySubscription = schema.getPerson.subscribe(id = "TestId12345") + + withTimeout(5.seconds) { + val resultsChannel = + Channel>(capacity = Channel.UNLIMITED) + val collectJob = launch { querySubscription.flow.collect(resultsChannel::send) } + + val maxHardwareConcurrency = Math.max(2, Runtime.getRuntime().availableProcessors()) + val multiThreadExecutor = Executors.newFixedThreadPool(maxHardwareConcurrency) + try { + repeat(100000) { multiThreadExecutor.execute(querySubscription::reload) } + } finally { + multiThreadExecutor.shutdown() + } + + var resultCount = 0 + while (true) { + resultCount++ + val result = withTimeoutOrNull(1.seconds) { resultsChannel.receive() } ?: break + assertThat(result.getOrThrow()).isEqualToGetPersonQueryResult(name = "Name", age = 10000) + } + assertThat(resultCount).isGreaterThan(0) + + collectJob.cancel() + } + } +} + +fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = + isEqualTo(PersonSchema.GetPersonQueryRef.Result(name = name, age = age)) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt index 0a5f045b855..560befacddd 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt @@ -48,9 +48,9 @@ class PostsTest { val postContent = Random.Default.nextLong().toString(30) runBlocking { - dc.mutations.createPost(id = postId, content = postContent).execute() + dc.mutations.createPost.execute(id = postId, content = postContent) - val queryResponse = dc.queries.getPost(id = postId).execute() + val queryResponse = dc.queries.getPost.execute(id = postId) assertWithMessage("queryResponse") .that(queryResponse.post) .isEqualTo(GetPostQuery.Result.Post(content = postContent, comments = emptyList())) @@ -67,23 +67,23 @@ class PostsTest { val postContent2 = Random.Default.nextLong().absoluteValue.toString(30) runBlocking { - dc.mutations.createPost(id = postId1, content = postContent1).execute() - dc.mutations.createPost(id = postId2, content = postContent2).execute() + dc.mutations.createPost.execute(id = postId1, content = postContent1) + dc.mutations.createPost.execute(id = postId2, content = postContent2) - val query = dc.queries.getPost(id = postId1) - assertWithMessage("lastResult 0").that(query.lastResult).isNull() + val querySubscription = dc.queries.getPost.subscribe(id = postId1) + assertWithMessage("lastResult 0").that(querySubscription.lastResult).isNull() - val result1 = query.subscribe().timeout(5.seconds).first() + val result1 = querySubscription.flow.timeout(5.seconds).first() assertWithMessage("result1.isSuccess").that(result1.isSuccess).isTrue() assertWithMessage("result1.post.content") .that(result1.getOrThrow().post.content) .isEqualTo(postContent1) - assertWithMessage("lastResult 1").that(query.lastResult).isEqualTo(result1) + assertWithMessage("lastResult 1").that(querySubscription.lastResult).isEqualTo(result1) - val flow2Job = async { query.subscribe().timeout(5.seconds).take(2).toList() } + val flow2Job = async { querySubscription.flow.timeout(5.seconds).take(2).toList() } - query.update(id = postId2) + querySubscription.update { id = postId2 } val results2 = flow2Job.await() assertWithMessage("results2.size").that(results2.size).isEqualTo(2) @@ -96,7 +96,7 @@ class PostsTest { .that(results2[1].getOrThrow().post.content) .isEqualTo(postContent2) - assertWithMessage("lastResult 2").that(query.lastResult).isEqualTo(results2[1]) + assertWithMessage("lastResult 2").that(querySubscription.lastResult).isEqualTo(results2[1]) } } } diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt index 794617c9101..d12a345d27b 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt @@ -1,17 +1,16 @@ package com.google.firebase.dataconnect.testutil +import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicLong import org.junit.rules.ExternalResource /** * A JUnit test rule that creates instances of an object for use during testing, and cleans them * upon test completion. */ -abstract class FactoryTestRule(startId: Long) : ExternalResource() { +abstract class FactoryTestRule : ExternalResource() { - private val nextId = AtomicLong(startId) private val active = AtomicBoolean(false) private val instances = CopyOnWriteArrayList() @@ -19,7 +18,7 @@ abstract class FactoryTestRule(startId: Long) : ExternalResource() { if (!active.get()) { throw IllegalStateException("newInstance() may only be called during the test's execution") } - val instance = createInstance(nextId.getAndIncrement().toString(16), params) + val instance = createInstance(generateRandomUid(), params) instances.add(instance) return instance } @@ -35,6 +34,11 @@ abstract class FactoryTestRule(startId: Long) : ExternalResource() { } } + private fun generateRandomUid(): String = + UUID.randomUUID().let { + it.leastSignificantBits.toString(30) + it.mostSignificantBits.toString(30) + } + protected abstract fun createInstance(instanceId: String, params: P?): T protected abstract fun destroyInstance(instance: T) } diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/IdentityCodec.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/IdentityCodec.kt new file mode 100644 index 00000000000..517efb59aa5 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/IdentityCodec.kt @@ -0,0 +1,37 @@ +package com.google.firebase.dataconnect.testutil + +import com.google.firebase.dataconnect.FirebaseDataConnect +import com.google.firebase.dataconnect.MutationRef +import com.google.firebase.dataconnect.QueryRef + +class IdentityMutationRef( + dataConnect: FirebaseDataConnect, + operationName: String, + operationSet: String, + revision: String +) : + MutationRef, Map>( + dataConnect = dataConnect, + operationName = operationName, + operationSet = operationSet, + revision = revision + ) { + override fun encodeVariables(variables: Map): Map = variables + override fun decodeResult(map: Map): Map = map +} + +class IdentityQueryRef( + dataConnect: FirebaseDataConnect, + operationName: String, + operationSet: String, + revision: String +) : + QueryRef, Map>( + dataConnect = dataConnect, + operationName = operationName, + operationSet = operationSet, + revision = revision + ) { + override fun encodeVariables(variables: Map): Map = variables + override fun decodeResult(map: Map): Map = map +} diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt index 322bf4be667..ad25ba258ed 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt @@ -21,7 +21,7 @@ import com.google.firebase.dataconnect.FirebaseDataConnect * closes them upon test completion. */ class TestDataConnectFactory : - FactoryTestRule(startId = 0xccdd0000000L) { + FactoryTestRule() { fun newInstance(location: String? = null, service: String? = null): FirebaseDataConnect = newInstance(Params(location = location, service = service)) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt index 20712c66bfd..0cce168bf80 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt @@ -9,7 +9,7 @@ import com.google.firebase.initialize * A JUnit test rule that creates instances of [FirebaseApp] for use during testing, and closes them * upon test completion. */ -class TestFirebaseAppFactory : FactoryTestRule(startId = 0xaabb0000000L) { +class TestFirebaseAppFactory : FactoryTestRule() { override fun createInstance(instanceId: String, params: Nothing?) = Firebase.initialize( diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt new file mode 100644 index 00000000000..276e7720040 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -0,0 +1,134 @@ +// Copyright 2023 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.dataconnect.testutil.schemas + +import com.google.firebase.dataconnect.FirebaseDataConnect +import com.google.firebase.dataconnect.MutationRef +import com.google.firebase.dataconnect.QueryRef +import com.google.firebase.dataconnect.testutil.installEmulatorSchema + +class PersonSchema(val dataConnect: FirebaseDataConnect) { + + suspend fun installEmulatorSchema() { + dataConnect.installEmulatorSchema("testing_graphql_schemas/person") + } + + class CreatePersonMutationRef(dataConnect: FirebaseDataConnect) : + MutationRef( + dataConnect = dataConnect, + operationName = "createPerson", + operationSet = "ops", + revision = "42" + ) { + + data class Variables(val id: String, val name: String, val age: Int? = null) + + override fun encodeVariables(variables: Variables): Map = + variables.run { + mapOf( + "data" to + buildMap { + put("id", id) + put("name", name) + age?.let { put("age", it) } + } + ) + } + + override fun decodeResult(map: Map) {} + } + + class UpdatePersonMutationRef(dataConnect: FirebaseDataConnect) : + MutationRef( + dataConnect = dataConnect, + operationName = "updatePerson", + operationSet = "ops", + revision = "42" + ) { + + data class Variables(val id: String, val name: String? = null, val age: Int? = null) + + override fun encodeVariables(variables: Variables): Map = + variables.run { + mapOf( + "id" to id, + "data" to + buildMap { + name?.let { put("name", it) } + age?.let { put("age", it) } + } + ) + } + + override fun decodeResult(map: Map) {} + } + + class GetPersonQueryRef(dataConnect: FirebaseDataConnect) : + QueryRef( + dataConnect = dataConnect, + operationName = "getPerson", + operationSet = "ops", + revision = "42" + ) { + + data class Variables(val id: String, val name: String? = null, val age: Int? = null) + data class Result(val name: String, val age: Int? = null) + + override fun encodeVariables(variables: Variables) = variables.run { mapOf("id" to id) } + + override fun decodeResult(map: Map) = + (map["person"] as Map<*, *>?)?.let { + Result(name = it["name"] as String, age = (it["age"] as Double?)?.toInt()) + } + } + + class DeletePersonMutationRef(dataConnect: FirebaseDataConnect) : + MutationRef( + dataConnect = dataConnect, + operationName = "deletePerson", + operationSet = "ops", + revision = "42" + ) { + + data class Variables(val id: String) + + override fun encodeVariables(variables: Variables) = variables.run { mapOf("id" to id) } + + override fun decodeResult(map: Map) {} + } + + val createPerson = CreatePersonMutationRef(dataConnect) + val updatePerson = UpdatePersonMutationRef(dataConnect) + val deletePerson = DeletePersonMutationRef(dataConnect) + val getPerson = GetPersonQueryRef(dataConnect) +} + +suspend fun PersonSchema.CreatePersonMutationRef.execute(id: String, name: String, age: Int?) = + execute(PersonSchema.CreatePersonMutationRef.Variables(id = id, name = name, age = age)) + +suspend fun PersonSchema.UpdatePersonMutationRef.execute( + id: String, + name: String? = null, + age: Int? = null +) = execute(PersonSchema.UpdatePersonMutationRef.Variables(id = id, name = name, age = age)) + +suspend fun PersonSchema.DeletePersonMutationRef.execute(id: String) = + execute(PersonSchema.DeletePersonMutationRef.Variables(id = id)) + +suspend fun PersonSchema.GetPersonQueryRef.execute(id: String) = + execute(PersonSchema.GetPersonQueryRef.Variables(id = id)) + +fun PersonSchema.GetPersonQueryRef.subscribe(id: String) = + subscribe(PersonSchema.GetPersonQueryRef.Variables(id = id)) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt index 2d289a30821..2bc7483ffd2 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt @@ -13,37 +13,17 @@ // limitations under the License. package com.google.firebase.dataconnect -import java.util.concurrent.atomic.AtomicReference - -abstract class BaseRef( +abstract class BaseRef +internal constructor( val dataConnect: FirebaseDataConnect, internal val operationName: String, internal val operationSet: String, - internal val revision: String, - variables: VariablesType + internal val revision: String ) { - private val _variables = AtomicReference(variables) - val variables: VariablesType - get() = _variables.get() - - abstract suspend fun execute(): ResultType - - fun update(newVariables: VariablesType) { - _variables.set(newVariables) - onUpdate() - } - - protected open fun onUpdate() {} - - protected interface Codec { - fun encodeVariables(variables: VariablesType): Map - fun decodeResult(map: Map): ResultType - } - - protected abstract val codec: Codec - - internal val variablesAsMap: Map - get() = codec.encodeVariables(variables) + abstract suspend fun execute(variables: VariablesType): ResultType + protected abstract fun encodeVariables(variables: VariablesType): Map + protected abstract fun decodeResult(map: Map): ResultType - internal fun resultFromMap(map: Map) = codec.decodeResult(map) + internal fun mapFromVariables(variables: VariablesType) = encodeVariables(variables) + internal fun resultFromMap(map: Map) = decodeResult(map) } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index d6e9e249183..22b38d3afbd 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -34,7 +34,7 @@ internal constructor( private val projectId: String, val location: String, val service: String, - backgroundDispatcher: CoroutineDispatcher, + internal val backgroundDispatcher: CoroutineDispatcher, private val creator: FirebaseDataConnectFactory ) : Closeable { @@ -106,32 +106,38 @@ internal constructor( } } - internal suspend fun executeQuery(ref: QueryRef): R = + internal suspend fun executeQuery(ref: QueryRef, variables: V): R = grpcClint .executeQuery( operationName = ref.operationName, operationSet = ref.operationSet, revision = ref.revision, - variables = ref.variablesAsMap, + variables = ref.mapFromVariables(variables) ) .let { response -> if (response.errors.isNotEmpty()) { - throw RuntimeException("query \"$ref.operationName\" failed: ${response.errors}") + throw QueryExecutionException( + "query \"$ref.operationName\" failed: ${response.errors}", + ref + ) } ref.resultFromMap(response.data) } - internal suspend fun executeMutation(ref: MutationRef): R = + internal suspend fun executeMutation(ref: MutationRef, variables: V): R = grpcClint .executeMutation( operationName = ref.operationName, operationSet = ref.operationSet, revision = ref.revision, - variables = ref.variablesAsMap + variables = ref.mapFromVariables(variables) ) .let { response -> if (response.errors.isNotEmpty()) { - throw RuntimeException("mutation \"${ref.operationName}\" failed: ${response.errors}") + throw MutationExecutionException( + "mutation \"${ref.operationName}\" failed: ${response.errors}", + ref + ) } ref.resultFromMap(response.data) } @@ -163,6 +169,13 @@ internal constructor( } } -open class DataConnectException(message: String) : Exception(message) +open class DataConnectException internal constructor(message: String) : Exception(message) -open class ResultDecodeError(message: String) : DataConnectException(message) +open class QueryExecutionException internal constructor(message: String, val ref: QueryRef<*, *>) : + DataConnectException(message) + +open class QueryResultDecodeException internal constructor(message: String, ref: QueryRef<*, *>) : + QueryExecutionException(message, ref) + +open class MutationExecutionException +internal constructor(message: String, val ref: MutationRef<*, *>) : DataConnectException(message) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt index 9dc4aff9d39..14752ae7edb 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt @@ -17,15 +17,14 @@ abstract class MutationRef( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, - revision: String, - variables: VariablesType + revision: String ) : BaseRef( dataConnect = dataConnect, operationName = operationName, operationSet = operationSet, - revision = revision, - variables = variables + revision = revision ) { - override suspend fun execute(): ResultType = dataConnect.executeMutation(this) + override suspend fun execute(variables: VariablesType): ResultType = + dataConnect.executeMutation(this, variables) } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt index 2d5d66ebb5d..51fece3d783 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt @@ -13,36 +13,21 @@ // limitations under the License. package com.google.firebase.dataconnect -import kotlinx.coroutines.flow.Flow - abstract class QueryRef( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, - revision: String, - variables: VariablesType + revision: String ) : BaseRef( dataConnect = dataConnect, operationName = operationName, operationSet = operationSet, - revision = revision, - variables = variables + revision = revision ) { - private val subscription: QuerySubscription by lazy { - QuerySubscription(this) - } - - val lastResult: Result? - get() = subscription.lastResult - - override suspend fun execute(): ResultType = dataConnect.executeQuery(this) - - fun subscribe(): Flow> = subscription.subscribe() - - fun reload(): Unit = subscription.reload() + override suspend fun execute(variables: VariablesType): ResultType = + dataConnect.executeQuery(this, variables) - override fun onUpdate() { - reload() - } + fun subscribe(variables: VariablesType): QuerySubscription = + QuerySubscription(this, variables) } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt index 70cfc2f7a24..853fa8ada20 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt @@ -13,173 +13,74 @@ // limitations under the License. package com.google.firebase.dataconnect -import java.util.concurrent.atomic.AtomicBoolean +import com.google.firebase.concurrent.FirebaseExecutors import java.util.concurrent.atomic.AtomicReference -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.job +import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.launch -internal class QuerySubscription -internal constructor(internal val query: QueryRef) { - // TODO: Call `coroutineScope.cancel()` when this object is no longer needed. - internal val coroutineScope = - CoroutineScope( - query.dataConnect.coroutineScope.coroutineContext.let { - it + SupervisorJob(it.job) + CoroutineName("QuerySubscriptionImpl") - } - ) - - private val eventLoop = QuerySubscriptionEventLoop(this) - - val lastResult: Result? - get() = eventLoop.lastResult.get() - - fun reload() = eventLoop.reload() - - fun subscribe() = - channelFlow { - eventLoop.registerSendChannel(channel) - awaitClose { eventLoop.unregisterSendChannel(channel) } - } - .buffer(Channel.UNLIMITED) - - init { - coroutineScope.launch { eventLoop.run() } - } -} - -private class QuerySubscriptionEventLoop( - private val subscription: QuerySubscription +class QuerySubscription +internal constructor( + internal val query: QueryRef, + variables: VariablesType ) { - - val lastResult = AtomicReference>() - - private val running = AtomicBoolean(false) - private val eventChannel = Channel(Channel.UNLIMITED) - private val registeredSendChannels = mutableListOf>>() - private var reloadJob: Job? = null + private val _variables = AtomicReference(variables) + val variables: VariablesType + get() = _variables.get() + + private val sharedFlow = + MutableSharedFlow>(replay = 1, extraBufferCapacity = Integer.MAX_VALUE) + private val sequentialDispatcher = + FirebaseExecutors.newSequentialExecutor(query.dataConnect.backgroundDispatcher.asExecutor()) + .asCoroutineDispatcher() + + // NOTE: The variables below must ONLY be accessed from coroutines that use `sequentialDispatcher` + // for their `CoroutineDispatcher`. Having this requirement removes the need for explicitly + // synchronizing access to these variables. + private var reloadInProgress = false private var pendingReload = false - suspend fun run() { - if (!running.compareAndSet(false, true)) { - throw IllegalStateException("run() has already been invoked") - } - - try { - for (event in eventChannel) { - processEvent(event) - } - } finally { - registeredSendChannels.clear() - reloadJob = null - } - } - - fun registerSendChannel(channel: SendChannel>) { - eventChannel.trySend(RegisterSendChannelEvent(channel)) - } - - fun unregisterSendChannel(channel: SendChannel>) { - eventChannel.trySend(UnregisterSendChannelEvent(channel)) - } + val lastResult + get() = sharedFlow.replayCache.firstOrNull() fun reload() { - eventChannel.trySend(ReloadEvent) - } - - private fun processEvent(event: Event): Unit = - when (event) { - is RegisterSendChannelEvent -> processRegisterSendChannelEvent(event) - is UnregisterSendChannelEvent -> processUnregisterSendChannelEvent(event) - is ReloadEvent -> processReloadEvent() - is ResultEvent -> processResultEvent(event) - } - - private fun processRegisterSendChannelEvent(event: RegisterSendChannelEvent) { - registeredSendChannels.add(event.typedChannel()) - - lastResult.get().also { - if (it != null) { - event.typedChannel>().trySend(it) - } else if (reloadJob == null) { - processReloadEvent() - } - } - } - - private fun processUnregisterSendChannelEvent(event: UnregisterSendChannelEvent) { - registeredSendChannels.listIterator().let { - while (it.hasNext()) { - if (it.next() === event.channel) { - it.remove() - break - } - } - } - } - - private fun processReloadEvent() { - if (reloadJob?.isActive == true) { + query.dataConnect.coroutineScope.launch(sequentialDispatcher) { pendingReload = true - return - } - - reloadJob = - subscription.coroutineScope.launch { - val result = - try { - Result.success(subscription.query.execute()) - } catch (e: Throwable) { - Result.failure(e) - } - - eventChannel.trySend(ResultEvent(result)) - } - } - - private fun processResultEvent(event: ResultEvent) { - if (pendingReload) { - pendingReload = false - reload() - } - - lastResult.set(event.typedResult()) - - registeredSendChannels.iterator().let { - while (it.hasNext()) { - val sendResult = it.next().trySend(event.typedResult()) - if (sendResult.isClosed) { - it.remove() + if (!reloadInProgress) { + reloadInProgress = true + try { + doReload() + } finally { + reloadInProgress = false } } } } - private sealed interface Event - private object ReloadEvent : Event - - private sealed class SendChannelEvent(val channel: SendChannel<*>) : Event { - @Suppress("UNCHECKED_CAST") - fun typedChannel(): SendChannel { - return channel as SendChannel - } + fun update(variables: VariablesType) { + _variables.set(variables) + reload() } - private class RegisterSendChannelEvent(channel: SendChannel<*>) : SendChannelEvent(channel) - private class UnregisterSendChannelEvent(channel: SendChannel<*>) : SendChannelEvent(channel) + val flow: Flow> + get() = sharedFlow.asSharedFlow().onSubscription { reload() }.buffer(Channel.CONFLATED) - private class ResultEvent(val result: Result<*>) : Event { - @Suppress("UNCHECKED_CAST") - fun typedResult(): Result { - return result as Result + private suspend fun doReload() { + while (pendingReload) { + pendingReload = false + val result = + try { + Result.success(query.execute(variables)) + } catch (e: Throwable) { + Result.failure(e) + } + sharedFlow.emit(result) } } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index 89b2b0b61a3..05b01b8db9c 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -1,68 +1,62 @@ package com.google.firebase.dataconnect.apiproposal -import kotlinx.coroutines.delay +import android.app.Activity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking //////////////////////////////////////////////////////////////////////////////////////////////////// // CORE SDK //////////////////////////////////////////////////////////////////////////////////////////////////// class FirebaseDataConnect { - - // NOTE: This interface and val are only needed if the alternative about adding a - // getPost() method by the generated SDK below is accepted. - class Queries internal constructor(val dataConnect: FirebaseDataConnect) + class Queries internal constructor() { + val dataConnect: FirebaseDataConnect + get() = TODO() + } val queries: Queries = TODO() } -open class DataConnectException(message: String) : Exception(message) -open class ResultDecodeError(message: String) : DataConnectException(message) +open class DataConnectException internal constructor(message: String) : Exception(message) -abstract class BaseRef( - val dataConnect: FirebaseDataConnect, - operationName: String, - operationSet: String, - revision: String, - variables: VariablesType -) { - val variables: VariablesType = TODO() - - abstract suspend fun execute(): ResultType - - protected open fun onUpdate() {} - - protected interface Codec { - fun encodeVariables(variables: VariablesType): Map - fun decodeResult(map: Map): ResultType - } +open class QueryExecutionException internal constructor(message: String, val ref: QueryRef<*, *>) : + DataConnectException(message) + +open class QueryResultDecodeException internal constructor(message: String, ref: QueryRef<*, *>) : + QueryExecutionException(message, ref) + +abstract class BaseRef internal constructor() { + val dataConnect: FirebaseDataConnect + get() = TODO() - protected abstract val codec: Codec + abstract suspend fun execute(variables: VariablesType): ResultType + + protected abstract fun encodeVariables(variables: VariablesType): Map + protected abstract fun decodeResult(map: Map): ResultType } abstract class QueryRef( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, - revision: String, - variables: VariablesType -) : - BaseRef( - dataConnect = dataConnect, - operationName = operationName, - operationSet = operationSet, - revision = revision, - variables = variables - ) { + revision: String +) : BaseRef() { + override suspend fun execute(variables: VariablesType): ResultType = TODO() + fun subscribe(variables: VariablesType): QuerySubscription = TODO() +} - // QUESTION: Should the result of execute() be delivered to the listeners that are subscribed? - // The current implementation does _not_ because it's difficult to know which is the "latest" - // result from concurrent calls to execute(). One workaround would be to serialize the calls to - // execute, but that could introduce unacceptable lag for the invocations at the end of the queue. - override suspend fun execute(): ResultType = TODO() +class QuerySubscription internal constructor() { + val query: QueryRef + get() = TODO() + val variables: VariablesType + get() = TODO() - fun subscribe(): Flow> = TODO() + // Alternative considered: add `lastResult`. The problem is, what do we do with this value if the + // variables are changed via a call to update()? Do we clear it? Or do we leave it there even + // though it came from a request with potentially-different variables? + val lastResult: Result? + get() = TODO() // Alternative considered: Return `Deferred>` so that customer knows when the reload // completes. For example, suppose a UI has a "Reload" button and when the customer clicks it they @@ -72,26 +66,28 @@ abstract class QueryRef( // some previous call to reload() by some other unrelated operation. fun reload(): Unit = TODO() - // Alternative considered: add `lastResult`. The problem is, what do we do with this value if the - // variables are changed? Do we clear it? Or do we leave it there even though it came from a - // request with different variables? - val lastResult: Result? get() = TODO() + fun flow(): Flow> = TODO() } //////////////////////////////////////////////////////////////////////////////////////////////////// // GENERATED SDK //////////////////////////////////////////////////////////////////////////////////////////////////// -class GetPostQuery(dataConnect: FirebaseDataConnect, variables: Variables) : +class GetPostQuery(dataConnect: FirebaseDataConnect) : QueryRef( - dataConnect = dataConnect, + dataConnect, operationName = "getPost", operationSet = "crud", - revision = "1234567890abcdef", - variables = variables, + revision = "1234567890abcdef" ) { - data class Variables(val id: String) + data class Variables(val id: String) { + val builder = Builder(id = id) + fun build(block: Builder.() -> Unit): Variables = builder.apply(block).build() + class Builder(var id: String) { + fun build() = Variables(id = id) + } + } data class Result(val post: Post) { data class Post(val content: String, val comments: List) { @@ -99,33 +95,66 @@ class GetPostQuery(dataConnect: FirebaseDataConnect, variables: Variables) : } } - override val codec = object : Codec { - override fun encodeVariables(variables: Variables) = TODO() - override fun decodeResult(map: Map) = TODO() - } + override fun encodeVariables(variables: Variables) = TODO() + + override fun decodeResult(map: Map) = TODO() } -fun FirebaseDataConnect.query(variables: GetPostQuery.Variables): GetPostQuery = - GetPostQuery(dataConnect = this, variables = variables) +typealias GetPostQuerySubscription = QuerySubscription -fun FirebaseDataConnect.Queries.getPost(id: String): GetPostQuery = - dataConnect.query(GetPostQuery.Variables(id = id)) +val FirebaseDataConnect.Queries.getPost: GetPostQuery + get() = TODO() + +suspend fun GetPostQuery.execute(id: String): GetPostQuery.Result = TODO() + +fun GetPostQuery.subscribe(id: String): GetPostQuerySubscription = TODO() //////////////////////////////////////////////////////////////////////////////////////////////////// // CUSTOMER CODE //////////////////////////////////////////////////////////////////////////////////////////////////// -fun demo(dataConnect: FirebaseDataConnect) = runBlocking { - val query = dataConnect.query(GetPostQuery.Variables(id = "abc")) +private class MainActivity : Activity() { + + private lateinit var dataConnect: FirebaseDataConnect + private lateinit var activityCoroutineScope: CoroutineScope + private var querySubscription: GetPostQuerySubscription? = null + private var querySubscriptionFlow: Job? = null + + fun onLiveUpdateButtonClick() { + if (querySubscription == null) { + querySubscription = + dataConnect.queries.getPost.subscribe(id = getIdFromTextView()).also { + querySubscriptionFlow = + activityCoroutineScope.launch { + it.flow().collect { + if (it.isFailure) { + showError(it.exceptionOrNull().toString()) + } else { + showPostContent(it.getOrThrow().post.content) + } + } + } + } + } + } + + fun onReloadButtonClick() { + querySubscription?.reload() + } - println(query.execute()) + fun onLoadButtonClick() { + activityCoroutineScope.launch { + val result = dataConnect.queries.getPost.execute(id = getIdFromTextView()) + showPostContent(result.post.content) + } + } - val subscribeJob1 = launch { query.subscribe().collect { println("Got result in job 1: $it") } } - val subscribeJob2 = launch { query.subscribe().collect { println("Got result in job 2: $it") } } + override fun onDestroy() { + querySubscriptionFlow?.cancel() + super.onDestroy() + } - delay(5000) - query.reload() - delay(5000) - subscribeJob1.cancel() - subscribeJob2.cancel() + fun getIdFromTextView(): String = TODO() + fun showError(errorMessage: String): Unit = TODO() + fun showPostContent(content: String): Unit = TODO() } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt index 498c5d8a894..a0e02a07761 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt @@ -16,36 +16,29 @@ package com.google.firebase.dataconnect.generated import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.MutationRef -class CreatePostMutation(dataConnect: FirebaseDataConnect, variables: Variables) : +class CreatePostMutation(dataConnect: FirebaseDataConnect) : MutationRef( dataConnect = dataConnect, operationName = "createPost", operationSet = "crud", revision = "1234567890abcdef", - variables = variables, ) { data class Variables(val data: PostData) { data class PostData(val id: String, val content: String) } - override val codec = - object : Codec { - override fun encodeVariables(variables: Variables) = - mapOf("data" to variables.data.run { mapOf("id" to id, "content" to content) }) + override fun encodeVariables(variables: Variables) = + mapOf("data" to variables.data.run { mapOf("id" to id, "content" to content) }) - override fun decodeResult(map: Map) {} - } + override fun decodeResult(map: Map) {} } -fun FirebaseDataConnect.mutation(variables: CreatePostMutation.Variables): CreatePostMutation = - CreatePostMutation(dataConnect = this, variables = variables) +val FirebaseDataConnect.Mutations.createPost + get() = CreatePostMutation(dataConnect) -fun FirebaseDataConnect.Mutations.createPost(id: String, content: String): CreatePostMutation = - dataConnect.mutation(variablesFor(id = id, content = content)) - -fun CreatePostMutation.update(id: String, content: String): Unit = - update(variablesFor(id = id, content = content)) +suspend fun CreatePostMutation.execute(id: String, content: String) = + execute(variablesFor(id = id, content = content)) private fun variablesFor(id: String, content: String) = CreatePostMutation.Variables( diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt index a40e8f28267..2b5f63b3060 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -15,18 +15,24 @@ package com.google.firebase.dataconnect.generated import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.QueryRef -import com.google.firebase.dataconnect.ResultDecodeError +import com.google.firebase.dataconnect.QueryResultDecodeException +import com.google.firebase.dataconnect.QuerySubscription -class GetPostQuery(dataConnect: FirebaseDataConnect, variables: Variables) : +class GetPostQuery(dataConnect: FirebaseDataConnect) : QueryRef( - dataConnect = dataConnect, + dataConnect, operationName = "getPost", operationSet = "crud", - revision = "1234567890abcdef", - variables = variables, + revision = "1234567890abcdef" ) { - data class Variables(val id: String) + data class Variables(val id: String) { + val builder = Builder(id = id) + fun build(block: Builder.() -> Unit): Variables = builder.apply(block).build() + class Builder(var id: String) { + fun build() = Variables(id = id) + } + } data class Result(val post: Post) { data class Post(val content: String, val comments: List) { @@ -34,71 +40,76 @@ class GetPostQuery(dataConnect: FirebaseDataConnect, variables: Variables) : } } - override val codec = - object : Codec { - override fun encodeVariables(variables: Variables) = mapOf("id" to variables.id) - - override fun decodeResult(map: Map) = - Result( - map["post"].let { - _decodePost( - data = it as? Map<*, *> ?: decodeError("post", it, "Map", "result", map), - path = "post" - ) - } - ) + override fun encodeVariables(variables: Variables) = mapOf("id" to variables.id) - private fun _decodePost(data: Map<*, *>, path: String) = - Result.Post( - content = - data["content"].let { - it as? String ?: decodeError("$path.content", it, "String", path, data) - }, - comments = - data["comments"].let { - _decodeComments( - it as? List<*> ?: decodeError("$path.comments", it, "List", path, data), - "$path.comments" - ) - } + override fun decodeResult(map: Map) = + Result( + map["post"].let { + _decodePost( + data = it as? Map<*, *> ?: decodeError("post", it, "Map", "result", map), + path = "post" ) + } + ) - private fun _decodeComments(data: List<*>, path: String) = - data.mapIndexed { index, it -> - _decodeComment( - it as? Map<*, *> ?: decodeError("$path[$index]", it, "Map", path, data), - "$path[$index]" + private fun _decodePost(data: Map<*, *>, path: String) = + Result.Post( + content = + data["content"].let { + it as? String ?: decodeError("$path.content", it, "String", path, data) + }, + comments = + data["comments"].let { + _decodeComments( + it as? List<*> ?: decodeError("$path.comments", it, "List", path, data), + "$path.comments" ) } + ) - private fun _decodeComment(data: Map<*, *>, path: String) = - Result.Post.Comment( - id = - data["id"].let { it as? String ?: decodeError("$path.id", it, "String", path, data) }, - content = - data["content"].let { - it as? String ?: decodeError("$path.content", it, "String", path, data) - } - ) - - private fun decodeError( - path: String, - actual: Any?, - expected: String, - contextName: String, - context: Any? - ): Nothing = - throw ResultDecodeError( - "parsing GetPostQuery.Result failed: \"$path\" was expected to be $expected, " + - "but got $actual ($contextName=$context)" - ) + private fun _decodeComments(data: List<*>, path: String) = + data.mapIndexed { index, it -> + _decodeComment( + it as? Map<*, *> ?: decodeError("$path[$index]", it, "Map", path, data), + "$path[$index]" + ) } + + private fun _decodeComment(data: Map<*, *>, path: String) = + Result.Post.Comment( + id = data["id"].let { it as? String ?: decodeError("$path.id", it, "String", path, data) }, + content = + data["content"].let { + it as? String ?: decodeError("$path.content", it, "String", path, data) + } + ) + + private fun decodeError( + path: String, + actual: Any?, + expected: String, + contextName: String, + context: Any? + ): Nothing = + throw QueryResultDecodeException( + "parsing GetPostQuery.Result failed: \"$path\" was expected to be $expected, " + + "but got $actual ($contextName=$context)", + this + ) } -fun FirebaseDataConnect.query(variables: GetPostQuery.Variables): GetPostQuery = - GetPostQuery(dataConnect = this, variables = variables) +typealias GetPostQuerySubscription = QuerySubscription + +val FirebaseDataConnect.Queries.getPost: GetPostQuery + get() = GetPostQuery(dataConnect) + +suspend fun GetPostQuery.execute(id: String): GetPostQuery.Result = execute(variablesFor(id = id)) + +fun GetPostQuery.subscribe( + id: String +): QuerySubscription = subscribe(variablesFor(id = id)) -fun FirebaseDataConnect.Queries.getPost(id: String): GetPostQuery = - dataConnect.query(GetPostQuery.Variables(id = id)) +fun GetPostQuerySubscription.update(block: GetPostQuery.Variables.Builder.() -> Unit) = + update(variables.build(block)) -fun GetPostQuery.update(id: String): Unit = update(GetPostQuery.Variables(id = id)) +private fun variablesFor(id: String) = GetPostQuery.Variables(id = id) From 2fdf53d001ea291f93182485c0a18fd93d863626 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 6 Nov 2023 21:39:07 -0500 Subject: [PATCH 036/573] QuerySubscriptionTest.kt: test cases added --- .../dataconnect/QuerySubscriptionTest.kt | 94 ++++++++++++++++++- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt index 37a8a38e8c3..1d34f30a086 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt @@ -19,10 +19,12 @@ package com.google.firebase.dataconnect import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Subject import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory import com.google.firebase.dataconnect.testutil.schemas.* import java.util.concurrent.Executors +import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.* import kotlinx.coroutines.channels.* @@ -61,7 +63,7 @@ class QuerySubscriptionTest { } @Test - fun flow_collect_should_trigger_reload() = runBlocking { + fun reload_should_notify_collecting_flows() = runBlocking { schema.createPerson.execute(id = "TestId12345", name = "Name0", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") @@ -86,7 +88,79 @@ class QuerySubscriptionTest { } @Test - fun calling_reload_many_times_concurrently_does_not_cause_issues() = runBlocking { + fun flow_collect_should_get_immediately_invoked_with_last_result() = runBlocking { + schema.createPerson.execute(id = "TestId12345", name = "TestName", age = 10000) + val querySubscription = schema.getPerson.subscribe(id = "TestId12345") + + withTimeout(2.seconds) { + val result1 = querySubscription.flow.first().getOrThrow() + assertThat(result1).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) + + schema.updatePerson.execute(id = "TestId12345", name = "TestName2", age = 10002) + + val result2 = querySubscription.flow.first().getOrThrow() + assertThat(result2).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) + } + } + + @Test + fun slow_flows_do_not_block_fast_flows() = runBlocking { + schema.createPerson.execute(id = "TestId12345", name = "TestName", age = 10000) + val querySubscription = schema.getPerson.subscribe(id = "TestId12345") + + withTimeout(2.seconds) { + val slowFlowJob = launch { + querySubscription.flow.collect { delay(Integer.MAX_VALUE.seconds) } + } + + repeat(5) { + assertWithMessage("fast flow retrieval iteration $it") + .that(querySubscription.flow.first().getOrThrow()) + .isEqualToGetPersonQueryResult(name = "TestName", age = 10000) + } + + slowFlowJob.cancel() + } + } + + @Test + fun reload_delivers_result_to_all_registered_flows() = runBlocking { + schema.createPerson.execute(id = "TestId12345", name = "TestName0", age = 10000) + val querySubscription = schema.getPerson.subscribe(id = "TestId12345") + + withTimeout(2.seconds) { + val resultsChannel1 = + Channel(capacity = Channel.UNLIMITED) + val flowJob1 = launch { + querySubscription.flow.collect { resultsChannel1.send(it.getOrThrow()) } + } + val resultsChannel2 = + Channel(capacity = Channel.UNLIMITED) + val flowJob2 = launch { + querySubscription.flow.collect { resultsChannel2.send(it.getOrThrow()) } + } + resultsChannel1.purge(0.25.seconds).forEach { + assertThat(it).isEqualToGetPersonQueryResult(name = "TestName0", age = 10000) + } + resultsChannel2.purge(0.25.seconds).forEach { + assertThat(it).isEqualToGetPersonQueryResult(name = "TestName0", age = 10000) + } + + schema.updatePerson.execute(id = "TestId12345", name = "TestName1", age = 10001) + querySubscription.reload() + + assertThat(resultsChannel1.receive()) + .isEqualToGetPersonQueryResult(name = "TestName1", age = 10001) + assertThat(resultsChannel2.receive()) + .isEqualToGetPersonQueryResult(name = "TestName1", age = 10001) + + flowJob1.cancel() + flowJob2.cancel() + } + } + + @Test + fun reload_concurrent_invocations_get_conflated() = runBlocking { schema.createPerson.execute(id = "TestId12345", name = "Name", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") @@ -116,5 +190,19 @@ class QuerySubscriptionTest { } } -fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = +private fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = isEqualTo(PersonSchema.GetPersonQueryRef.Result(name = name, age = age)) + +private suspend fun ReceiveChannel.purge(timeout: Duration): List = coroutineScope { + mutableListOf() + .also { + while (true) { + try { + withTimeout(timeout) { it.add(receive()) } + } catch (e: TimeoutCancellationException) { + break + } + } + } + .toList() +} From 0201103a95215483fb49be893c753a8e37877301 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 6 Nov 2023 22:02:38 -0500 Subject: [PATCH 037/573] use appropriate executors and dispatchers --- .../dataconnect/DataConnectGrpcClient.kt | 8 +++---- .../dataconnect/FirebaseDataConnect.kt | 19 ++++++++++------ .../dataconnect/FirebaseDataConnectFactory.kt | 9 ++++---- .../FirebaseDataConnectRegistrar.kt | 22 +++++++++---------- .../firebase/dataconnect/QuerySubscription.kt | 3 +-- 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index d6e84aee720..8a496eac275 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -28,9 +28,8 @@ import google.internal.firebase.firemat.v0.executeQueryRequest import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder +import java.util.concurrent.Executor import java.util.concurrent.TimeUnit -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asExecutor internal class DataConnectGrpcClient( val context: Context, @@ -40,6 +39,7 @@ internal class DataConnectGrpcClient( val hostName: String, val port: Int, val sslEnabled: Boolean, + executor: Executor, creatorLoggerId: String, ) { private val logger = Logger("DataConnectGrpcClient") @@ -73,9 +73,7 @@ internal class DataConnectGrpcClient( // failsafe. it.keepAliveTime(30, TimeUnit.SECONDS) - // TODO: Create a dedicated executor rather than using a global one. - // See go/kotlin/coroutines/coroutine-contexts-scopes.md for details. - it.executor(Dispatchers.IO.asExecutor()) + it.executor(executor) // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel to // respond more gracefully to network change events, such as switching from cellular to wifi. diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 22b38d3afbd..9a45b2888f0 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -18,13 +18,14 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app import java.io.Closeable +import java.util.concurrent.Executor import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read import kotlin.concurrent.write -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel class FirebaseDataConnect @@ -34,7 +35,8 @@ internal constructor( private val projectId: String, val location: String, val service: String, - internal val backgroundDispatcher: CoroutineDispatcher, + internal val blockingExecutor: Executor, + internal val nonBlockingExecutor: Executor, private val creator: FirebaseDataConnectFactory ) : Closeable { @@ -54,7 +56,9 @@ internal constructor( internal val coroutineScope = CoroutineScope( - SupervisorJob() + backgroundDispatcher + CoroutineName("FirebaseDataConnectImpl") + SupervisorJob() + + nonBlockingExecutor.asCoroutineDispatcher() + + CoroutineName("FirebaseDataConnect") ) private val lock = ReentrantReadWriteLock() @@ -84,7 +88,7 @@ internal constructor( settings = settings.builder.build(block) } - private val grpcClint: DataConnectGrpcClient by lazy { + private val grpcClient: DataConnectGrpcClient by lazy { logger.debug { "DataConnectGrpcClient initialization started" } lock.write { if (closed) { @@ -100,6 +104,7 @@ internal constructor( hostName = settings.hostName, port = settings.port, sslEnabled = settings.sslEnabled, + executor = blockingExecutor, creatorLoggerId = logger.id, ) .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } @@ -107,7 +112,7 @@ internal constructor( } internal suspend fun executeQuery(ref: QueryRef, variables: V): R = - grpcClint + grpcClient .executeQuery( operationName = ref.operationName, operationSet = ref.operationSet, @@ -125,7 +130,7 @@ internal constructor( } internal suspend fun executeMutation(ref: MutationRef, variables: V): R = - grpcClint + grpcClient .executeMutation( operationName = ref.operationName, operationSet = ref.operationSet, @@ -147,7 +152,7 @@ internal constructor( lock.write { coroutineScope.cancel() try { - grpcClint.close() + grpcClient.close() } finally { closed = true creator.remove(this) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt index 48f77d70ca6..a2a2be88611 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt @@ -16,15 +16,15 @@ package com.google.firebase.dataconnect import android.content.Context import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseAppLifecycleListener +import java.util.concurrent.Executor import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock -import kotlinx.coroutines.CoroutineDispatcher internal class FirebaseDataConnectFactory( private val context: Context, private val firebaseApp: FirebaseApp, - private val backgroundDispatcher: CoroutineDispatcher, - private val blockingDispatcher: CoroutineDispatcher, + private val blockingExecutor: Executor, + private val nonBlockingExecutor: Executor, ) { private val firebaseAppLifecycleListener = FirebaseAppLifecycleListener { _, _ -> close() } @@ -60,7 +60,8 @@ internal class FirebaseDataConnectFactory( projectId = projectId, location = location, service = service, - backgroundDispatcher = backgroundDispatcher, + blockingExecutor = blockingExecutor, + nonBlockingExecutor = nonBlockingExecutor, creator = this ) instancesByCacheKey[key] = newInstance diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt index 7d0134b7385..6a50d6a850d 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt @@ -18,15 +18,15 @@ import android.content.Context import androidx.annotation.Keep import androidx.annotation.RestrictTo import com.google.firebase.FirebaseApp -import com.google.firebase.annotations.concurrent.Background import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.annotations.concurrent.Lightweight import com.google.firebase.components.Component import com.google.firebase.components.ComponentRegistrar import com.google.firebase.components.Dependency import com.google.firebase.components.Qualified.qualified import com.google.firebase.components.Qualified.unqualified import com.google.firebase.platforminfo.LibraryVersionComponent -import kotlinx.coroutines.CoroutineDispatcher +import java.util.concurrent.Executor /** * [ComponentRegistrar] for setting up [FirebaseDataConnect]. @@ -44,14 +44,14 @@ internal class FirebaseDataConnectRegistrar : ComponentRegistrar { .name(LIBRARY_NAME) .add(Dependency.required(firebaseApp)) .add(Dependency.required(context)) - .add(Dependency.required(backgroundDispatcher)) - .add(Dependency.required(blockingDispatcher)) + .add(Dependency.required(blockingExecutor)) + .add(Dependency.required(nonBlockingExecutor)) .factory { container -> FirebaseDataConnectFactory( - container.get(context), - container.get(firebaseApp), - container.get(backgroundDispatcher), - container.get(blockingDispatcher), + context = container.get(context), + firebaseApp = container.get(firebaseApp), + blockingExecutor = container.get(blockingExecutor), + nonBlockingExecutor = container.get(nonBlockingExecutor), ) } .build(), @@ -63,9 +63,7 @@ internal class FirebaseDataConnectRegistrar : ComponentRegistrar { private val firebaseApp = unqualified(FirebaseApp::class.java) private val context = unqualified(Context::class.java) - private val backgroundDispatcher = - qualified(Background::class.java, CoroutineDispatcher::class.java) - private val blockingDispatcher = - qualified(Blocking::class.java, CoroutineDispatcher::class.java) + private val blockingExecutor = qualified(Blocking::class.java, Executor::class.java) + private val nonBlockingExecutor = qualified(Lightweight::class.java, Executor::class.java) } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt index 853fa8ada20..5c197f66c56 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt @@ -16,7 +16,6 @@ package com.google.firebase.dataconnect import com.google.firebase.concurrent.FirebaseExecutors import java.util.concurrent.atomic.AtomicReference import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -37,7 +36,7 @@ internal constructor( private val sharedFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = Integer.MAX_VALUE) private val sequentialDispatcher = - FirebaseExecutors.newSequentialExecutor(query.dataConnect.backgroundDispatcher.asExecutor()) + FirebaseExecutors.newSequentialExecutor(query.dataConnect.nonBlockingExecutor) .asCoroutineDispatcher() // NOTE: The variables below must ONLY be accessed from coroutines that use `sequentialDispatcher` From f4dae448792c5ea21db1bfc99947d5b38873342f Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 6 Nov 2023 22:25:53 -0500 Subject: [PATCH 038/573] exception hierarchy cleanup --- .../dataconnect/DataConnectGrpcClient.kt | 22 ++++++++++++---- .../dataconnect/FirebaseDataConnect.kt | 26 +++++++------------ .../dataconnect/generated/GetPostQuery.kt | 7 +++-- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 8a496eac275..19d140543bc 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -91,7 +91,7 @@ internal class DataConnectGrpcClient( operationSet: String, operationName: String, revision: String, - variables: Map + variables: Map, ): ExecuteQueryResult { val request = executeQueryRequest { this.name = name(operationSet = operationSet, revision = revision) @@ -100,7 +100,13 @@ internal class DataConnectGrpcClient( } logger.debug { "executeQuery() sending request: $request" } - val response = grpcStub.executeQuery(request) + val response = + try { + grpcStub.executeQuery(request) + } catch (e: Throwable) { + logger.warn { "executeQuery() network transport error: $e" } + throw NetworkTransportException("query network transport error: ${e.message}", e) + } logger.debug { "executeQuery() got response: $response" } return ExecuteQueryResult( data = mapFromStruct(response.data), @@ -121,7 +127,13 @@ internal class DataConnectGrpcClient( } logger.debug { "executeMutation() sending request: $request" } - val response = grpcStub.executeMutation(request) + val response = + try { + grpcStub.executeMutation(request) + } catch (e: Throwable) { + logger.warn { "executeMutation() network transport error: $e" } + throw NetworkTransportException("mutation network transport error: ${e.message}", e) + } logger.debug { "executeMutation() got response: $response" } return ExecuteMutationResult( data = mapFromStruct(response.data), @@ -158,7 +170,7 @@ private fun objectFromStructValue(struct: Value): Any? = Value.KindCase.STRING_VALUE -> stringValue Value.KindCase.LIST_VALUE -> listValue.valuesList.map { objectFromStructValue(it) } Value.KindCase.STRUCT_VALUE -> mapFromStruct(structValue) - else -> throw IllegalArgumentException("unsupported Struct kind: $kindCase") + else -> throw ResultDecodeException("unsupported Struct kind: $kindCase") } } @@ -185,6 +197,6 @@ private fun valueFromObject(obj: Any?): Value = value { } is Iterable<*> -> listValue = obj.let { listValue { it.forEach { values.add(valueFromObject(it)) } } } - else -> throw IllegalArgumentException("unsupported value type: ${obj::class}") + else -> throw ResultDecodeException("unsupported value type: ${obj::class} ($obj)") } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 9a45b2888f0..0dd0a36be1c 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -121,10 +121,7 @@ internal constructor( ) .let { response -> if (response.errors.isNotEmpty()) { - throw QueryExecutionException( - "query \"$ref.operationName\" failed: ${response.errors}", - ref - ) + throw ExecutionException("query \"$ref.operationName\" failed: ${response.errors}") } ref.resultFromMap(response.data) } @@ -135,14 +132,11 @@ internal constructor( operationName = ref.operationName, operationSet = ref.operationSet, revision = ref.revision, - variables = ref.mapFromVariables(variables) + variables = ref.mapFromVariables(variables), ) .let { response -> if (response.errors.isNotEmpty()) { - throw MutationExecutionException( - "mutation \"${ref.operationName}\" failed: ${response.errors}", - ref - ) + throw ExecutionException("mutation \"${ref.operationName}\" failed: ${response.errors}") } ref.resultFromMap(response.data) } @@ -174,13 +168,13 @@ internal constructor( } } -open class DataConnectException internal constructor(message: String) : Exception(message) +open class DataConnectException internal constructor(message: String, cause: Throwable? = null) : + Exception(message, cause) -open class QueryExecutionException internal constructor(message: String, val ref: QueryRef<*, *>) : - DataConnectException(message) +open class NetworkTransportException internal constructor(message: String, cause: Throwable) : + DataConnectException(message, cause) -open class QueryResultDecodeException internal constructor(message: String, ref: QueryRef<*, *>) : - QueryExecutionException(message, ref) +open class ExecutionException internal constructor(message: String) : DataConnectException(message) -open class MutationExecutionException -internal constructor(message: String, val ref: MutationRef<*, *>) : DataConnectException(message) +open class ResultDecodeException internal constructor(message: String) : + DataConnectException(message) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt index 2b5f63b3060..ab3a1b64acc 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -15,8 +15,8 @@ package com.google.firebase.dataconnect.generated import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.QueryRef -import com.google.firebase.dataconnect.QueryResultDecodeException import com.google.firebase.dataconnect.QuerySubscription +import com.google.firebase.dataconnect.ResultDecodeException class GetPostQuery(dataConnect: FirebaseDataConnect) : QueryRef( @@ -91,10 +91,9 @@ class GetPostQuery(dataConnect: FirebaseDataConnect) : contextName: String, context: Any? ): Nothing = - throw QueryResultDecodeException( + throw ResultDecodeException( "parsing GetPostQuery.Result failed: \"$path\" was expected to be $expected, " + - "but got $actual ($contextName=$context)", - this + "but got $actual ($contextName=$context)" ) } From 3da3febf5d293d32e6d111429db0be2c98cdb105 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 6 Nov 2023 22:34:26 -0500 Subject: [PATCH 039/573] fix another exception --- .../firebase/dataconnect/DataConnectGrpcClient.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 19d140543bc..05f61a7d2af 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -190,13 +190,21 @@ private fun valueFromObject(obj: Any?): Value = value { obj.let { struct { it.forEach { entry -> - val key = entry.key as? String ?: error("unsupported map key: $entry.key") + val key = + entry.key.let { key -> + key as? String + ?: throw ResultDecodeException( + "unsupported map key: " + + if (key == null) "null" else "${key::class.qualifiedName} (${key})" + ) + } fields.put(key, valueFromObject(entry.value)) } } } is Iterable<*> -> listValue = obj.let { listValue { it.forEach { values.add(valueFromObject(it)) } } } - else -> throw ResultDecodeException("unsupported value type: ${obj::class} ($obj)") + else -> + throw ResultDecodeException("unsupported value type: ${obj::class.qualifiedName} ($obj)") } } From ee59e8700b015957405d3706957a8a430b142495 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 6 Nov 2023 22:36:46 -0500 Subject: [PATCH 040/573] QueryApiProposal.kt: update with some changes --- .../dataconnect/apiproposal/QueryApiProposal.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index 05b01b8db9c..23e05e20fe1 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -18,13 +18,16 @@ class FirebaseDataConnect { val queries: Queries = TODO() } -open class DataConnectException internal constructor(message: String) : Exception(message) +open class DataConnectException internal constructor(message: String, cause: Throwable? = null) : + Exception(message, cause) -open class QueryExecutionException internal constructor(message: String, val ref: QueryRef<*, *>) : - DataConnectException(message) +open class NetworkTransportException internal constructor(message: String, cause: Throwable) : + DataConnectException(message, cause) + +open class ExecutionException internal constructor(message: String) : DataConnectException(message) -open class QueryResultDecodeException internal constructor(message: String, ref: QueryRef<*, *>) : - QueryExecutionException(message, ref) +open class ResultDecodeException internal constructor(message: String) : + DataConnectException(message) abstract class BaseRef internal constructor() { val dataConnect: FirebaseDataConnect @@ -66,7 +69,7 @@ class QuerySubscription internal constructor() { // some previous call to reload() by some other unrelated operation. fun reload(): Unit = TODO() - fun flow(): Flow> = TODO() + val flow: Flow> = TODO() } //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -126,7 +129,7 @@ private class MainActivity : Activity() { dataConnect.queries.getPost.subscribe(id = getIdFromTextView()).also { querySubscriptionFlow = activityCoroutineScope.launch { - it.flow().collect { + it.flow.collect { if (it.isFailure) { showError(it.exceptionOrNull().toString()) } else { From a324da5e5e236fdf855726a3f282babc6a41b760 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 7 Nov 2023 01:22:31 -0500 Subject: [PATCH 041/573] add star imports for kotlin concurrent and coroutines --- .../firebase/dataconnect/FirebaseDataConnect.kt | 9 ++------- .../google/firebase/dataconnect/QuerySubscription.kt | 11 +++-------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 0dd0a36be1c..6def30c15fc 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -20,13 +20,8 @@ import com.google.firebase.app import java.io.Closeable import java.util.concurrent.Executor import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.read -import kotlin.concurrent.write -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancel +import kotlin.concurrent.* +import kotlinx.coroutines.* class FirebaseDataConnect internal constructor( diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt index 5c197f66c56..b8a09fc273a 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt @@ -15,14 +15,9 @@ package com.google.firebase.dataconnect import com.google.firebase.concurrent.FirebaseExecutors import java.util.concurrent.atomic.AtomicReference -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.onSubscription -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* class QuerySubscription internal constructor( From be37174354de3d0aa8aa7719f62f332d1defbe85 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 7 Nov 2023 11:25:49 -0500 Subject: [PATCH 042/573] bring back codec --- .../dataconnect/FirebaseDataConnectTest.kt | 28 +-- .../dataconnect/QuerySubscriptionTest.kt | 15 +- .../dataconnect/testutil/IdentityCodec.kt | 33 +--- .../testutil/schemas/PersonSchema.kt | 173 ++++++++++-------- .../google/firebase/dataconnect/BaseRef.kt | 11 +- .../dataconnect/DataConnectGrpcClient.kt | 31 ++-- .../dataconnect/FirebaseDataConnect.kt | 29 +-- .../firebase/dataconnect/MutationRef.kt | 8 +- .../google/firebase/dataconnect/QueryRef.kt | 8 +- .../apiproposal/QueryApiProposal.kt | 55 +++--- .../generated/CreatePostMutation.kt | 34 ++-- .../dataconnect/generated/GetPostQuery.kt | 146 ++++++++------- 12 files changed, 298 insertions(+), 273 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 61cd61750c0..91d02276057 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -21,8 +21,7 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule -import com.google.firebase.dataconnect.testutil.IdentityMutationRef -import com.google.firebase.dataconnect.testutil.IdentityQueryRef +import com.google.firebase.dataconnect.testutil.IdentityCodec import com.google.firebase.dataconnect.testutil.TestDataConnectFactory import com.google.firebase.dataconnect.testutil.TestFirebaseAppFactory import com.google.firebase.dataconnect.testutil.installEmulatorSchema @@ -204,11 +203,12 @@ class FirebaseDataConnectTest { run { val mutation = - IdentityMutationRef( + MutationRef( dataConnect = dc, operationName = "createPost", operationSet = "crud", - revision = "TestRevision" + revision = "TestRevision", + codec = IdentityCodec, ) val mutationResponse = mutation.execute(mapOf("data" to mapOf("id" to postId, "content" to postContent))) @@ -219,11 +219,12 @@ class FirebaseDataConnectTest { run { val query = - IdentityQueryRef( + QueryRef( dataConnect = dc, operationName = "getPost", operationSet = "crud", - revision = "TestRevision" + revision = "TestRevision", + codec = IdentityCodec, ) val queryResult = query.execute(mapOf("id" to postId)) assertWithMessage("queryResponse") @@ -237,11 +238,12 @@ class FirebaseDataConnectTest { @Test fun testInstallEmulatorSchema() { suspend fun FirebaseDataConnect.createPerson(id: String, name: String, age: Int? = null) = - IdentityMutationRef( + MutationRef( dataConnect = this, operationName = "createPerson", operationSet = "ops", - revision = "42" + revision = "42", + codec = IdentityCodec, ) .execute( mapOf( @@ -255,20 +257,22 @@ class FirebaseDataConnectTest { ) suspend fun FirebaseDataConnect.getPerson(id: String) = - IdentityQueryRef( + QueryRef( dataConnect = this, operationName = "getPerson", operationSet = "ops", - revision = "42" + revision = "42", + codec = IdentityCodec, ) .execute(mapOf("id" to id)) suspend fun FirebaseDataConnect.getAllPeople() = - IdentityQueryRef( + QueryRef( dataConnect = this, operationName = "getAllPeople", operationSet = "ops", - revision = "42" + revision = "42", + codec = IdentityCodec, ) .execute(emptyMap()) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt index 1d34f30a086..4dc0f61520a 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt @@ -22,7 +22,10 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory -import com.google.firebase.dataconnect.testutil.schemas.* +import com.google.firebase.dataconnect.testutil.schemas.CreatePersonMutationExt.execute +import com.google.firebase.dataconnect.testutil.schemas.GetPersonQueryExt.subscribe +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema +import com.google.firebase.dataconnect.testutil.schemas.UpdatePersonMutationExt.execute import java.util.concurrent.Executors import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -69,7 +72,7 @@ class QuerySubscriptionTest { withTimeout(2.seconds) { val resultsChannel = - Channel(capacity = Channel.UNLIMITED) + Channel(capacity = Channel.UNLIMITED) val collectJob = launch { querySubscription.flow.collect { resultsChannel.send(it.getOrThrow()) } } @@ -130,12 +133,12 @@ class QuerySubscriptionTest { withTimeout(2.seconds) { val resultsChannel1 = - Channel(capacity = Channel.UNLIMITED) + Channel(capacity = Channel.UNLIMITED) val flowJob1 = launch { querySubscription.flow.collect { resultsChannel1.send(it.getOrThrow()) } } val resultsChannel2 = - Channel(capacity = Channel.UNLIMITED) + Channel(capacity = Channel.UNLIMITED) val flowJob2 = launch { querySubscription.flow.collect { resultsChannel2.send(it.getOrThrow()) } } @@ -166,7 +169,7 @@ class QuerySubscriptionTest { withTimeout(5.seconds) { val resultsChannel = - Channel>(capacity = Channel.UNLIMITED) + Channel>(capacity = Channel.UNLIMITED) val collectJob = launch { querySubscription.flow.collect(resultsChannel::send) } val maxHardwareConcurrency = Math.max(2, Runtime.getRuntime().availableProcessors()) @@ -191,7 +194,7 @@ class QuerySubscriptionTest { } private fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = - isEqualTo(PersonSchema.GetPersonQueryRef.Result(name = name, age = age)) + isEqualTo(PersonSchema.GetPersonQuery.Result(name = name, age = age)) private suspend fun ReceiveChannel.purge(timeout: Duration): List = coroutineScope { mutableListOf() diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/IdentityCodec.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/IdentityCodec.kt index 517efb59aa5..d208e731bf8 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/IdentityCodec.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/IdentityCodec.kt @@ -1,37 +1,8 @@ package com.google.firebase.dataconnect.testutil -import com.google.firebase.dataconnect.FirebaseDataConnect -import com.google.firebase.dataconnect.MutationRef -import com.google.firebase.dataconnect.QueryRef +import com.google.firebase.dataconnect.BaseRef -class IdentityMutationRef( - dataConnect: FirebaseDataConnect, - operationName: String, - operationSet: String, - revision: String -) : - MutationRef, Map>( - dataConnect = dataConnect, - operationName = operationName, - operationSet = operationSet, - revision = revision - ) { - override fun encodeVariables(variables: Map): Map = variables - override fun decodeResult(map: Map): Map = map -} - -class IdentityQueryRef( - dataConnect: FirebaseDataConnect, - operationName: String, - operationSet: String, - revision: String -) : - QueryRef, Map>( - dataConnect = dataConnect, - operationName = operationName, - operationSet = operationSet, - revision = revision - ) { +object IdentityCodec : BaseRef.Codec, Map> { override fun encodeVariables(variables: Map): Map = variables override fun decodeResult(map: Map): Map = map } diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index 276e7720040..e3bade69da4 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -14,6 +14,7 @@ package com.google.firebase.dataconnect.testutil.schemas +import com.google.firebase.dataconnect.BaseRef import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.MutationRef import com.google.firebase.dataconnect.QueryRef @@ -25,110 +26,138 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { dataConnect.installEmulatorSchema("testing_graphql_schemas/person") } - class CreatePersonMutationRef(dataConnect: FirebaseDataConnect) : - MutationRef( + val createPerson = + MutationRef( dataConnect = dataConnect, operationName = "createPerson", operationSet = "ops", - revision = "42" - ) { + revision = "42", + codec = CreatePersonMutation, + ) + + class CreatePersonMutation private constructor() { data class Variables(val id: String, val name: String, val age: Int? = null) - override fun encodeVariables(variables: Variables): Map = - variables.run { - mapOf( - "data" to - buildMap { - put("id", id) - put("name", name) - age?.let { put("age", it) } - } - ) - } - - override fun decodeResult(map: Map) {} + internal companion object Codec : BaseRef.Codec { + override fun encodeVariables(variables: Variables): Map = + variables.run { + mapOf( + "data" to + buildMap { + put("id", id) + put("name", name) + age?.let { put("age", it) } + } + ) + } + + override fun decodeResult(map: Map) {} + } } - class UpdatePersonMutationRef(dataConnect: FirebaseDataConnect) : - MutationRef( + val updatePerson = + MutationRef( dataConnect = dataConnect, operationName = "updatePerson", operationSet = "ops", - revision = "42" - ) { + revision = "42", + codec = UpdatePersonMutation, + ) + + class UpdatePersonMutation private constructor() { data class Variables(val id: String, val name: String? = null, val age: Int? = null) - override fun encodeVariables(variables: Variables): Map = - variables.run { - mapOf( - "id" to id, - "data" to - buildMap { - name?.let { put("name", it) } - age?.let { put("age", it) } - } - ) - } - - override fun decodeResult(map: Map) {} + internal companion object Codec : BaseRef.Codec { + + override fun encodeVariables(variables: Variables): Map = + variables.run { + mapOf( + "id" to id, + "data" to + buildMap { + name?.let { put("name", it) } + age?.let { put("age", it) } + } + ) + } + + override fun decodeResult(map: Map) {} + } } - class GetPersonQueryRef(dataConnect: FirebaseDataConnect) : - QueryRef( + val deletePerson = + MutationRef( dataConnect = dataConnect, - operationName = "getPerson", + operationName = "deletePerson", operationSet = "ops", - revision = "42" - ) { + revision = "42", + codec = DeletePersonMutation, + ) - data class Variables(val id: String, val name: String? = null, val age: Int? = null) - data class Result(val name: String, val age: Int? = null) + class DeletePersonMutation private constructor() { + + data class Variables(val id: String) - override fun encodeVariables(variables: Variables) = variables.run { mapOf("id" to id) } + internal companion object Codec : BaseRef.Codec { - override fun decodeResult(map: Map) = - (map["person"] as Map<*, *>?)?.let { - Result(name = it["name"] as String, age = (it["age"] as Double?)?.toInt()) - } + override fun encodeVariables(variables: Variables) = variables.run { mapOf("id" to id) } + + override fun decodeResult(map: Map) {} + } } - class DeletePersonMutationRef(dataConnect: FirebaseDataConnect) : - MutationRef( + val getPerson = + QueryRef( dataConnect = dataConnect, - operationName = "deletePerson", + operationName = "getPerson", operationSet = "ops", - revision = "42" - ) { + revision = "42", + codec = GetPersonQuery, + ) - data class Variables(val id: String) + class GetPersonQuery private constructor() { - override fun encodeVariables(variables: Variables) = variables.run { mapOf("id" to id) } + data class Variables(val id: String, val name: String? = null, val age: Int? = null) + data class Result(val name: String, val age: Int? = null) - override fun decodeResult(map: Map) {} - } + internal companion object Codec : BaseRef.Codec { + override fun encodeVariables(variables: Variables) = variables.run { mapOf("id" to id) } - val createPerson = CreatePersonMutationRef(dataConnect) - val updatePerson = UpdatePersonMutationRef(dataConnect) - val deletePerson = DeletePersonMutationRef(dataConnect) - val getPerson = GetPersonQueryRef(dataConnect) + override fun decodeResult(map: Map) = + (map["person"] as Map<*, *>?)?.let { + Result(name = it["name"] as String, age = (it["age"] as Double?)?.toInt()) + } + } + } } -suspend fun PersonSchema.CreatePersonMutationRef.execute(id: String, name: String, age: Int?) = - execute(PersonSchema.CreatePersonMutationRef.Variables(id = id, name = name, age = age)) +object CreatePersonMutationExt { + suspend fun MutationRef.execute( + id: String, + name: String, + age: Int? + ) = execute(PersonSchema.CreatePersonMutation.Variables(id = id, name = name, age = age)) +} -suspend fun PersonSchema.UpdatePersonMutationRef.execute( - id: String, - name: String? = null, - age: Int? = null -) = execute(PersonSchema.UpdatePersonMutationRef.Variables(id = id, name = name, age = age)) +object UpdatePersonMutationExt { + suspend fun MutationRef.execute( + id: String, + name: String? = null, + age: Int? = null + ) = execute(PersonSchema.UpdatePersonMutation.Variables(id = id, name = name, age = age)) +} -suspend fun PersonSchema.DeletePersonMutationRef.execute(id: String) = - execute(PersonSchema.DeletePersonMutationRef.Variables(id = id)) +object DeletePersonMutationExt { + suspend fun MutationRef.execute(id: String) = + execute(PersonSchema.DeletePersonMutation.Variables(id = id)) +} -suspend fun PersonSchema.GetPersonQueryRef.execute(id: String) = - execute(PersonSchema.GetPersonQueryRef.Variables(id = id)) +object GetPersonQueryExt { + suspend fun QueryRef + .execute(id: String) = execute(PersonSchema.GetPersonQuery.Variables(id = id)) -fun PersonSchema.GetPersonQueryRef.subscribe(id: String) = - subscribe(PersonSchema.GetPersonQueryRef.Variables(id = id)) + fun QueryRef + .subscribe(id: String) = subscribe(PersonSchema.GetPersonQuery.Variables(id = id)) +} diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt index 2bc7483ffd2..3e592e96c59 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt @@ -18,12 +18,13 @@ internal constructor( val dataConnect: FirebaseDataConnect, internal val operationName: String, internal val operationSet: String, - internal val revision: String + internal val revision: String, + internal val codec: Codec, ) { abstract suspend fun execute(variables: VariablesType): ResultType - protected abstract fun encodeVariables(variables: VariablesType): Map - protected abstract fun decodeResult(map: Map): ResultType - internal fun mapFromVariables(variables: VariablesType) = encodeVariables(variables) - internal fun resultFromMap(map: Map) = decodeResult(map) + interface Codec { + fun encodeVariables(variables: VariablesType): Map + fun decodeResult(map: Map): ResultType + } } diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 05f61a7d2af..a76e4379b96 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -22,7 +22,6 @@ import com.google.protobuf.listValue import com.google.protobuf.struct import com.google.protobuf.value import google.internal.firebase.firemat.v0.DataServiceGrpcKt.DataServiceCoroutineStub -import google.internal.firebase.firemat.v0.DataServiceOuterClass.GraphqlError import google.internal.firebase.firemat.v0.executeMutationRequest import google.internal.firebase.firemat.v0.executeQueryRequest import io.grpc.ManagedChannel @@ -83,16 +82,12 @@ internal class DataConnectGrpcClient( private val grpcStub: DataServiceCoroutineStub by lazy { DataServiceCoroutineStub(grpcChannel) } - data class ExecuteQueryResult(val data: Map, val errors: List) - - data class ExecuteMutationResult(val data: Map, val errors: List) - suspend fun executeQuery( operationSet: String, operationName: String, revision: String, variables: Map, - ): ExecuteQueryResult { + ): Map { val request = executeQueryRequest { this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName @@ -108,10 +103,13 @@ internal class DataConnectGrpcClient( throw NetworkTransportException("query network transport error: ${e.message}", e) } logger.debug { "executeQuery() got response: $response" } - return ExecuteQueryResult( - data = mapFromStruct(response.data), - errors = response.errorsList, - ) + if (response.errorsList.isNotEmpty()) { + throw GraphQLException( + "query failed: ${response.errorsList}", + response.errorsList.map { it.toString() } + ) + } + return mapFromStruct(response.data) } suspend fun executeMutation( @@ -119,7 +117,7 @@ internal class DataConnectGrpcClient( operationName: String, revision: String, variables: Map - ): ExecuteMutationResult { + ): Map { val request = executeMutationRequest { this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName @@ -135,10 +133,13 @@ internal class DataConnectGrpcClient( throw NetworkTransportException("mutation network transport error: ${e.message}", e) } logger.debug { "executeMutation() got response: $response" } - return ExecuteMutationResult( - data = mapFromStruct(response.data), - errors = response.errorsList, - ) + if (response.errorsList.isNotEmpty()) { + throw GraphQLException( + "mutation failed: ${response.errorsList}", + response.errorsList.map { it.toString() } + ) + } + return mapFromStruct(response.data) } override fun toString(): String { diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 6def30c15fc..e9ef3af9a2f 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -107,34 +107,24 @@ internal constructor( } internal suspend fun executeQuery(ref: QueryRef, variables: V): R = - grpcClient - .executeQuery( + ref.codec.decodeResult( + grpcClient.executeQuery( operationName = ref.operationName, operationSet = ref.operationSet, revision = ref.revision, - variables = ref.mapFromVariables(variables) + variables = ref.codec.encodeVariables(variables) ) - .let { response -> - if (response.errors.isNotEmpty()) { - throw ExecutionException("query \"$ref.operationName\" failed: ${response.errors}") - } - ref.resultFromMap(response.data) - } + ) internal suspend fun executeMutation(ref: MutationRef, variables: V): R = - grpcClient - .executeMutation( + ref.codec.decodeResult( + grpcClient.executeMutation( operationName = ref.operationName, operationSet = ref.operationSet, revision = ref.revision, - variables = ref.mapFromVariables(variables), + variables = ref.codec.encodeVariables(variables) ) - .let { response -> - if (response.errors.isNotEmpty()) { - throw ExecutionException("mutation \"${ref.operationName}\" failed: ${response.errors}") - } - ref.resultFromMap(response.data) - } + ) override fun close() { logger.debug { "close() called" } @@ -169,7 +159,8 @@ open class DataConnectException internal constructor(message: String, cause: Thr open class NetworkTransportException internal constructor(message: String, cause: Throwable) : DataConnectException(message, cause) -open class ExecutionException internal constructor(message: String) : DataConnectException(message) +open class GraphQLException internal constructor(message: String, val errors: List) : + DataConnectException(message) open class ResultDecodeException internal constructor(message: String) : DataConnectException(message) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt index 14752ae7edb..87c9165c6b8 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt @@ -13,17 +13,19 @@ // limitations under the License. package com.google.firebase.dataconnect -abstract class MutationRef( +class MutationRef( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, - revision: String + revision: String, + codec: Codec, ) : BaseRef( dataConnect = dataConnect, operationName = operationName, operationSet = operationSet, - revision = revision + revision = revision, + codec = codec, ) { override suspend fun execute(variables: VariablesType): ResultType = dataConnect.executeMutation(this, variables) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt index 51fece3d783..e58219ecdff 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt @@ -13,17 +13,19 @@ // limitations under the License. package com.google.firebase.dataconnect -abstract class QueryRef( +class QueryRef( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, - revision: String + revision: String, + codec: Codec, ) : BaseRef( dataConnect = dataConnect, operationName = operationName, operationSet = operationSet, - revision = revision + revision = revision, + codec = codec, ) { override suspend fun execute(variables: VariablesType): ResultType = dataConnect.executeQuery(this, variables) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index 23e05e20fe1..186606e0af1 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -35,17 +35,21 @@ abstract class BaseRef internal constructor() { abstract suspend fun execute(variables: VariablesType): ResultType - protected abstract fun encodeVariables(variables: VariablesType): Map - protected abstract fun decodeResult(map: Map): ResultType + interface Codec { + fun encodeVariables(variables: VariablesType): Map + fun decodeResult(map: Map): ResultType + } } -abstract class QueryRef( +class QueryRef( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, - revision: String + revision: String, + codec: Codec, ) : BaseRef() { override suspend fun execute(variables: VariablesType): ResultType = TODO() + fun subscribe(variables: VariablesType): QuerySubscription = TODO() } @@ -76,21 +80,9 @@ class QuerySubscription internal constructor() { // GENERATED SDK //////////////////////////////////////////////////////////////////////////////////////////////////// -class GetPostQuery(dataConnect: FirebaseDataConnect) : - QueryRef( - dataConnect, - operationName = "getPost", - operationSet = "crud", - revision = "1234567890abcdef" - ) { - - data class Variables(val id: String) { - val builder = Builder(id = id) - fun build(block: Builder.() -> Unit): Variables = builder.apply(block).build() - class Builder(var id: String) { - fun build() = Variables(id = id) - } - } +class GetPostQuery private constructor() { + + data class Variables(val id: String) data class Result(val post: Post) { data class Post(val content: String, val comments: List) { @@ -98,19 +90,25 @@ class GetPostQuery(dataConnect: FirebaseDataConnect) : } } - override fun encodeVariables(variables: Variables) = TODO() - - override fun decodeResult(map: Map) = TODO() + companion object { + fun query(dataConnect: FirebaseDataConnect): QueryRef = TODO() + } } -typealias GetPostQuerySubscription = QuerySubscription - -val FirebaseDataConnect.Queries.getPost: GetPostQuery +val FirebaseDataConnect.Queries.getPost: QueryRef get() = TODO() -suspend fun GetPostQuery.execute(id: String): GetPostQuery.Result = TODO() +suspend fun QueryRef.execute( + id: String +): GetPostQuery.Result = TODO() -fun GetPostQuery.subscribe(id: String): GetPostQuerySubscription = TODO() +fun QueryRef.subscribe( + id: String +): QuerySubscription = TODO() + +typealias GetPostQueryRef = QueryRef + +typealias GetPostQuerySubscription = QuerySubscription //////////////////////////////////////////////////////////////////////////////////////////////////// // CUSTOMER CODE @@ -120,7 +118,8 @@ private class MainActivity : Activity() { private lateinit var dataConnect: FirebaseDataConnect private lateinit var activityCoroutineScope: CoroutineScope - private var querySubscription: GetPostQuerySubscription? = null + private var querySubscription: QuerySubscription? = + null private var querySubscriptionFlow: Job? = null fun onLiveUpdateButtonClick() { diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt index a0e02a07761..5d739ae6720 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt @@ -13,31 +13,41 @@ // limitations under the License. package com.google.firebase.dataconnect.generated +import com.google.firebase.dataconnect.BaseRef import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.MutationRef -class CreatePostMutation(dataConnect: FirebaseDataConnect) : - MutationRef( - dataConnect = dataConnect, - operationName = "createPost", - operationSet = "crud", - revision = "1234567890abcdef", - ) { +class CreatePostMutation private constructor() { data class Variables(val data: PostData) { data class PostData(val id: String, val content: String) } - override fun encodeVariables(variables: Variables) = - mapOf("data" to variables.data.run { mapOf("id" to id, "content" to content) }) + companion object { - override fun decodeResult(map: Map) {} + fun mutation(dataConnect: FirebaseDataConnect) = + MutationRef( + dataConnect = dataConnect, + operationName = "createPost", + operationSet = "crud", + revision = "1234567890abcdef", + codec = codec + ) + + private val codec = + object : BaseRef.Codec { + override fun encodeVariables(variables: Variables) = + mapOf("data" to variables.data.run { mapOf("id" to id, "content" to content) }) + + override fun decodeResult(map: Map) {} + } + } } val FirebaseDataConnect.Mutations.createPost - get() = CreatePostMutation(dataConnect) + get() = CreatePostMutation.mutation(dataConnect) -suspend fun CreatePostMutation.execute(id: String, content: String) = +suspend fun MutationRef.execute(id: String, content: String) = execute(variablesFor(id = id, content = content)) private fun variablesFor(id: String, content: String) = diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt index ab3a1b64acc..b102375ebaf 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -13,18 +13,13 @@ // limitations under the License. package com.google.firebase.dataconnect.generated +import com.google.firebase.dataconnect.BaseRef import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.QueryRef import com.google.firebase.dataconnect.QuerySubscription import com.google.firebase.dataconnect.ResultDecodeException -class GetPostQuery(dataConnect: FirebaseDataConnect) : - QueryRef( - dataConnect, - operationName = "getPost", - operationSet = "crud", - revision = "1234567890abcdef" - ) { +class GetPostQuery private constructor() { data class Variables(val id: String) { val builder = Builder(id = id) @@ -40,75 +35,92 @@ class GetPostQuery(dataConnect: FirebaseDataConnect) : } } - override fun encodeVariables(variables: Variables) = mapOf("id" to variables.id) + companion object { - override fun decodeResult(map: Map) = - Result( - map["post"].let { - _decodePost( - data = it as? Map<*, *> ?: decodeError("post", it, "Map", "result", map), - path = "post" - ) - } - ) - - private fun _decodePost(data: Map<*, *>, path: String) = - Result.Post( - content = - data["content"].let { - it as? String ?: decodeError("$path.content", it, "String", path, data) - }, - comments = - data["comments"].let { - _decodeComments( - it as? List<*> ?: decodeError("$path.comments", it, "List", path, data), - "$path.comments" - ) - } - ) - - private fun _decodeComments(data: List<*>, path: String) = - data.mapIndexed { index, it -> - _decodeComment( - it as? Map<*, *> ?: decodeError("$path[$index]", it, "Map", path, data), - "$path[$index]" + fun query(dataConnect: FirebaseDataConnect) = + QueryRef( + dataConnect, + operationName = "getPost", + operationSet = "crud", + revision = "1234567890abcdef", + codec = codec ) - } - private fun _decodeComment(data: Map<*, *>, path: String) = - Result.Post.Comment( - id = data["id"].let { it as? String ?: decodeError("$path.id", it, "String", path, data) }, - content = - data["content"].let { - it as? String ?: decodeError("$path.content", it, "String", path, data) - } - ) - - private fun decodeError( - path: String, - actual: Any?, - expected: String, - contextName: String, - context: Any? - ): Nothing = - throw ResultDecodeException( - "parsing GetPostQuery.Result failed: \"$path\" was expected to be $expected, " + - "but got $actual ($contextName=$context)" - ) + private val codec = + object : BaseRef.Codec { + override fun encodeVariables(variables: Variables) = mapOf("id" to variables.id) + + override fun decodeResult(map: Map) = + Result( + map["post"].let { + _decodePost( + data = it as? Map<*, *> ?: decodeError("post", it, "Map", "result", map), + path = "post" + ) + } + ) + + private fun _decodePost(data: Map<*, *>, path: String) = + Result.Post( + content = + data["content"].let { + it as? String ?: decodeError("$path.content", it, "String", path, data) + }, + comments = + data["comments"].let { + _decodeComments( + it as? List<*> ?: decodeError("$path.comments", it, "List", path, data), + "$path.comments" + ) + } + ) + + private fun _decodeComments(data: List<*>, path: String) = + data.mapIndexed { index, it -> + _decodeComment( + it as? Map<*, *> ?: decodeError("$path[$index]", it, "Map", path, data), + "$path[$index]" + ) + } + + private fun _decodeComment(data: Map<*, *>, path: String) = + Result.Post.Comment( + id = + data["id"].let { it as? String ?: decodeError("$path.id", it, "String", path, data) }, + content = + data["content"].let { + it as? String ?: decodeError("$path.content", it, "String", path, data) + } + ) + + private fun decodeError( + path: String, + actual: Any?, + expected: String, + contextName: String, + context: Any? + ): Nothing = + throw ResultDecodeException( + "parsing GetPostQuery.Result failed: \"$path\" was expected to be $expected, " + + "but got $actual ($contextName=$context)" + ) + } + } } typealias GetPostQuerySubscription = QuerySubscription -val FirebaseDataConnect.Queries.getPost: GetPostQuery - get() = GetPostQuery(dataConnect) +val FirebaseDataConnect.Queries.getPost + get() = GetPostQuery.query(dataConnect) -suspend fun GetPostQuery.execute(id: String): GetPostQuery.Result = execute(variablesFor(id = id)) +suspend fun QueryRef.execute(id: String) = + execute(variablesFor(id = id)) -fun GetPostQuery.subscribe( - id: String -): QuerySubscription = subscribe(variablesFor(id = id)) +fun QueryRef.subscribe(id: String) = + subscribe(variablesFor(id = id)) -fun GetPostQuerySubscription.update(block: GetPostQuery.Variables.Builder.() -> Unit) = - update(variables.build(block)) +fun QuerySubscription.update( + block: GetPostQuery.Variables.Builder.() -> Unit +) = update(variables.build(block)) private fun variablesFor(id: String) = GetPostQuery.Variables(id = id) From 2800045eb1887b3c2c9c907937e7f28785d1d5a4 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 7 Nov 2023 13:50:27 -0500 Subject: [PATCH 043/573] configure dokka --- firebase-dataconnect/firebase-dataconnect.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts index 8ec3feac53f..0ea8c5a9c59 100644 --- a/firebase-dataconnect/firebase-dataconnect.gradle.kts +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import org.jetbrains.dokka.gradle.DokkaTask + plugins { id("firebase-library") id("kotlin-android") @@ -118,3 +120,7 @@ tasks.withType().all { extra["packageName"] = "com.google.firebase.dataconnect" apply(from = "../gradle/googleServices.gradle") + +tasks.withType().configureEach { + moduleName.set("firebase-dataconnect") +} \ No newline at end of file From fecdd50f836982085ec8dd7b242aa1e66ef77b1a Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 7 Nov 2023 13:59:35 -0500 Subject: [PATCH 044/573] ktdoc added --- .../-base-ref/-codec/decode-result.md | 6 + .../-base-ref/-codec/encode-variables.md | 6 + .../-base-ref/-codec/index.md | 13 ++ .../-base-ref/data-connect.md | 6 + .../-base-ref/execute.md | 6 + .../-base-ref/index.md | 29 ++++ .../-data-connect-exception/index.md | 33 ++++ .../-execution-exception/index.md | 26 +++ .../-firebase-data-connect.md | 6 + .../-queries/data-connect.md | 6 + .../-firebase-data-connect/-queries/index.md | 13 ++ .../-firebase-data-connect/index.md | 24 +++ .../-firebase-data-connect/queries.md | 6 + .../-get-post-query-ref/index.md | 6 + .../-get-post-query-subscription/index.md | 6 + .../-get-post-query/-companion/index.md | 12 ++ .../-get-post-query/-companion/query.md | 6 + .../-result/-post/-comment/-comment.md | 6 + .../-result/-post/-comment/content.md | 6 + .../-result/-post/-comment/id.md | 6 + .../-result/-post/-comment/index.md | 19 ++ .../-get-post-query/-result/-post/-post.md | 6 + .../-get-post-query/-result/-post/comments.md | 6 + .../-get-post-query/-result/-post/content.md | 6 + .../-get-post-query/-result/-post/index.md | 25 +++ .../-get-post-query/-result/-result.md | 6 + .../-get-post-query/-result/index.md | 24 +++ .../-get-post-query/-result/post.md | 6 + .../-get-post-query/-variables/-variables.md | 6 + .../-get-post-query/-variables/id.md | 6 + .../-get-post-query/-variables/index.md | 18 ++ .../-get-post-query/index.md | 14 ++ .../-network-transport-exception/index.md | 26 +++ .../-query-ref/-query-ref.md | 6 + .../-query-ref/execute.md | 6 + .../-query-ref/index.md | 27 +++ .../-query-ref/subscribe.md | 6 + .../-query-subscription/flow.md | 6 + .../-query-subscription/index.md | 21 +++ .../-query-subscription/last-result.md | 6 + .../-query-subscription/query.md | 6 + .../-query-subscription/reload.md | 6 + .../-query-subscription/variables.md | 6 + .../-result-decode-exception/index.md | 26 +++ .../execute.md | 6 + .../get-post.md | 6 + .../index.md | 32 ++++ .../subscribe.md | 6 + .../-create-post-mutation/-companion/index.md | 12 ++ .../-companion/mutation.md | 6 + .../-variables/-post-data/-post-data.md | 6 + .../-variables/-post-data/content.md | 6 + .../-variables/-post-data/id.md | 6 + .../-variables/-post-data/index.md | 19 ++ .../-variables/-variables.md | 6 + .../-create-post-mutation/-variables/data.md | 6 + .../-create-post-mutation/-variables/index.md | 24 +++ .../-create-post-mutation/index.md | 13 ++ .../-get-post-query-subscription/index.md | 6 + .../-get-post-query/-companion/index.md | 12 ++ .../-get-post-query/-companion/query.md | 6 + .../-result/-post/-comment/-comment.md | 6 + .../-result/-post/-comment/content.md | 6 + .../-result/-post/-comment/id.md | 6 + .../-result/-post/-comment/index.md | 19 ++ .../-get-post-query/-result/-post/-post.md | 6 + .../-get-post-query/-result/-post/comments.md | 6 + .../-get-post-query/-result/-post/content.md | 6 + .../-get-post-query/-result/-post/index.md | 25 +++ .../-get-post-query/-result/-result.md | 6 + .../-get-post-query/-result/index.md | 24 +++ .../-get-post-query/-result/post.md | 6 + .../-variables/-builder/-builder.md | 6 + .../-variables/-builder/build.md | 6 + .../-get-post-query/-variables/-builder/id.md | 6 + .../-variables/-builder/index.md | 24 +++ .../-get-post-query/-variables/-variables.md | 6 + .../-get-post-query/-variables/build.md | 6 + .../-get-post-query/-variables/builder.md | 6 + .../-get-post-query/-variables/id.md | 6 + .../-get-post-query/-variables/index.md | 31 ++++ .../-get-post-query/index.md | 14 ++ .../create-post.md | 6 + .../execute.md | 8 + .../get-post.md | 6 + .../index.md | 26 +++ .../subscribe.md | 6 + .../update.md | 6 + .../-base-ref/-codec/decode-result.md | 6 + .../-base-ref/-codec/encode-variables.md | 6 + .../-base-ref/-codec/index.md | 13 ++ .../-base-ref/data-connect.md | 6 + .../-base-ref/execute.md | 6 + .../-base-ref/index.md | 30 ++++ .../-data-connect-exception/index.md | 33 ++++ .../-builder/build.md | 8 + .../-builder/connect-to-emulator.md | 6 + .../-builder/host-name.md | 6 + .../-builder/index.md | 21 +++ .../-builder/port.md | 6 + .../-builder/ssl-enabled.md | 6 + .../-companion/defaults.md | 6 + .../-companion/index.md | 12 ++ .../builder.md | 6 + .../-firebase-data-connect-settings/equals.md | 6 + .../hash-code.md | 6 + .../host-name.md | 6 + .../-firebase-data-connect-settings/index.md | 30 ++++ .../-firebase-data-connect-settings/port.md | 6 + .../ssl-enabled.md | 6 + .../to-string.md | 6 + .../-companion/get-instance.md | 8 + .../-companion/index.md | 12 ++ .../-mutations/data-connect.md | 6 + .../-mutations/index.md | 13 ++ .../-queries/data-connect.md | 6 + .../-firebase-data-connect/-queries/index.md | 13 ++ .../-firebase-data-connect/app.md | 6 + .../-firebase-data-connect/close.md | 6 + .../-firebase-data-connect/index.md | 33 ++++ .../-firebase-data-connect/location.md | 6 + .../-firebase-data-connect/mutations.md | 6 + .../-firebase-data-connect/queries.md | 6 + .../-firebase-data-connect/service.md | 6 + .../-firebase-data-connect/settings.md | 6 + .../-firebase-data-connect/to-string.md | 6 + .../-firebase-data-connect/update-settings.md | 6 + .../-graph-q-l-exception/errors.md | 6 + .../-graph-q-l-exception/index.md | 27 +++ .../-log-level/-d-e-b-u-g/index.md | 13 ++ .../-log-level/-i-n-f-o/index.md | 13 ++ .../-log-level/-w-a-r-n-i-n-g/index.md | 13 ++ .../-log-level/entries.md | 10 ++ .../-log-level/index.md | 29 ++++ .../-log-level/value-of.md | 14 ++ .../-log-level/values.md | 10 ++ .../-mutation-ref/-mutation-ref.md | 6 + .../-mutation-ref/execute.md | 6 + .../-mutation-ref/index.md | 25 +++ .../-network-transport-exception/index.md | 26 +++ .../-query-ref/-query-ref.md | 6 + .../-query-ref/execute.md | 6 + .../-query-ref/index.md | 27 +++ .../-query-ref/subscribe.md | 6 + .../-query-subscription/flow.md | 6 + .../-query-subscription/index.md | 22 +++ .../-query-subscription/last-result.md | 6 + .../-query-subscription/reload.md | 6 + .../-query-subscription/update.md | 6 + .../-query-subscription/variables.md | 6 + .../-result-decode-exception/index.md | 26 +++ .../com.google.firebase.dataconnect/index.md | 25 +++ .../log-level.md | 9 + .../ktdoc/firebase-dataconnect/package-list | 162 ++++++++++++++++++ firebase-dataconnect/ktdoc/index.md | 11 ++ 155 files changed, 1897 insertions(+) create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/decode-result.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/encode-variables.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/data-connect.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/execute.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-data-connect-exception/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-execution-exception/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-firebase-data-connect.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/data-connect.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/queries.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-ref/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-subscription/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/query.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/-comment.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/content.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/id.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-post.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/comments.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/content.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-result.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/post.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/-variables.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/id.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-network-transport-exception/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/-query-ref.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/execute.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/subscribe.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/flow.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/last-result.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/query.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/reload.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/variables.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/execute.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/get-post.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/subscribe.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/mutation.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/-post-data.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/content.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/id.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-variables.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/data.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query-subscription/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/query.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/-comment.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/content.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/id.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-post.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/comments.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/content.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-result.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/post.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/-builder.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/build.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/id.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-variables.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/build.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/builder.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/id.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/create-post.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/get-post.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/subscribe.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/update.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/decode-result.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/encode-variables.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/data-connect.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/execute.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-data-connect-exception/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/connect-to-emulator.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/host-name.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/port.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/ssl-enabled.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/defaults.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/builder.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/equals.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/hash-code.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/host-name.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/port.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/ssl-enabled.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/to-string.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/data-connect.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/data-connect.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/app.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/close.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/location.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/mutations.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/queries.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/service.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/settings.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/to-string.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/update-settings.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/errors.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-d-e-b-u-g/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-i-n-f-o/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-w-a-r-n-i-n-g/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/entries.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/value-of.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/values.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/-mutation-ref.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/execute.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-network-transport-exception/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/-query-ref.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/execute.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/subscribe.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/flow.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/last-result.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/reload.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/update.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/variables.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-result-decode-exception/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/index.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/log-level.md create mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/package-list create mode 100644 firebase-dataconnect/ktdoc/index.md diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/decode-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/decode-result.md new file mode 100644 index 00000000000..411b4365e98 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/decode-result.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[BaseRef](../index.md)/[Codec](index.md)/[decodeResult](decode-result.md) + +# decodeResult + +[androidJvm]\ +abstract fun [decodeResult](decode-result.md)(map: [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?>): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/encode-variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/encode-variables.md new file mode 100644 index 00000000000..9b968ef204c --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/encode-variables.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[BaseRef](../index.md)/[Codec](index.md)/[encodeVariables](encode-variables.md) + +# encodeVariables + +[androidJvm]\ +abstract fun [encodeVariables](encode-variables.md)(variables: [VariablesType](index.md)): [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/index.md new file mode 100644 index 00000000000..4228102b68f --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/index.md @@ -0,0 +1,13 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[BaseRef](../index.md)/[Codec](index.md) + +# Codec + +[androidJvm]\ +interface [Codec](index.md)<[VariablesType](index.md), [ResultType](index.md)> + +## Functions + +| Name | Summary | +|---|---| +| [decodeResult](decode-result.md) | [androidJvm]
abstract fun [decodeResult](decode-result.md)(map: [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?>): [ResultType](index.md) | +| [encodeVariables](encode-variables.md) | [androidJvm]
abstract fun [encodeVariables](encode-variables.md)(variables: [VariablesType](index.md)): [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/data-connect.md new file mode 100644 index 00000000000..906a5aa7a69 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/data-connect.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[BaseRef](index.md)/[dataConnect](data-connect.md) + +# dataConnect + +[androidJvm]\ +val [dataConnect](data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/execute.md new file mode 100644 index 00000000000..9982802b47c --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/execute.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[BaseRef](index.md)/[execute](execute.md) + +# execute + +[androidJvm]\ +abstract suspend fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/index.md new file mode 100644 index 00000000000..c65636aa336 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/index.md @@ -0,0 +1,29 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[BaseRef](index.md) + +# BaseRef + +abstract class [BaseRef](index.md)<[VariablesType](index.md), [ResultType](index.md)> + +#### Inheritors + +| | +|---| +| [QueryRef](../-query-ref/index.md) | + +## Types + +| Name | Summary | +|---|---| +| [Codec](-codec/index.md) | [androidJvm]
interface [Codec](-codec/index.md)<[VariablesType](-codec/index.md), [ResultType](-codec/index.md)> | + +## Properties + +| Name | Summary | +|---|---| +| [dataConnect](data-connect.md) | [androidJvm]
val [dataConnect](data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) | + +## Functions + +| Name | Summary | +|---|---| +| [execute](execute.md) | [androidJvm]
abstract suspend fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-data-connect-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-data-connect-exception/index.md new file mode 100644 index 00000000000..b2b95719497 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-data-connect-exception/index.md @@ -0,0 +1,33 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[DataConnectException](index.md) + +# DataConnectException + +open class [DataConnectException](index.md) : [Exception](https://developer.android.com/reference/kotlin/java/lang/Exception.html) + +#### Inheritors + +| | +|---| +| [NetworkTransportException](../-network-transport-exception/index.md) | +| [ExecutionException](../-execution-exception/index.md) | +| [ResultDecodeException](../-result-decode-exception/index.md) | + +## Properties + +| Name | Summary | +|---|---| +| [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | +| [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | + +## Functions + +| Name | Summary | +|---|---| +| [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | +| [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | +| [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | +| [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | +| [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-execution-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-execution-exception/index.md new file mode 100644 index 00000000000..7a864c2f0ea --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-execution-exception/index.md @@ -0,0 +1,26 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[ExecutionException](index.md) + +# ExecutionException + +[androidJvm]\ +open class [ExecutionException](index.md) : [DataConnectException](../-data-connect-exception/index.md) + +## Properties + +| Name | Summary | +|---|---| +| [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | +| [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | + +## Functions + +| Name | Summary | +|---|---| +| [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | +| [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | +| [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | +| [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | +| [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-firebase-data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-firebase-data-connect.md new file mode 100644 index 00000000000..bb2ccf24f3e --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-firebase-data-connect.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[FirebaseDataConnect](index.md)/[FirebaseDataConnect](-firebase-data-connect.md) + +# FirebaseDataConnect + +[androidJvm]\ +constructor() diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/data-connect.md new file mode 100644 index 00000000000..54332a376ab --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/data-connect.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[FirebaseDataConnect](../index.md)/[Queries](index.md)/[dataConnect](data-connect.md) + +# dataConnect + +[androidJvm]\ +val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/index.md new file mode 100644 index 00000000000..5f8e52dc795 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/index.md @@ -0,0 +1,13 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[FirebaseDataConnect](../index.md)/[Queries](index.md) + +# Queries + +[androidJvm]\ +class [Queries](index.md) + +## Properties + +| Name | Summary | +|---|---| +| [dataConnect](data-connect.md) | [androidJvm]
val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) | +| [getPost](../../get-post.md) | [androidJvm]
val [FirebaseDataConnect.Queries](index.md).[getPost](../../get-post.md): [QueryRef](../../-query-ref/index.md)<[GetPostQuery.Variables](../../-get-post-query/-variables/index.md), [GetPostQuery.Result](../../-get-post-query/-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/index.md new file mode 100644 index 00000000000..faf9a3e2e26 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/index.md @@ -0,0 +1,24 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[FirebaseDataConnect](index.md) + +# FirebaseDataConnect + +[androidJvm]\ +class [FirebaseDataConnect](index.md) + +## Constructors + +| | | +|---|---| +| [FirebaseDataConnect](-firebase-data-connect.md) | [androidJvm]
constructor() | + +## Types + +| Name | Summary | +|---|---| +| [Queries](-queries/index.md) | [androidJvm]
class [Queries](-queries/index.md) | + +## Properties + +| Name | Summary | +|---|---| +| [queries](queries.md) | [androidJvm]
val [queries](queries.md): [FirebaseDataConnect.Queries](-queries/index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/queries.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/queries.md new file mode 100644 index 00000000000..7c18a83196c --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/queries.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[FirebaseDataConnect](index.md)/[queries](queries.md) + +# queries + +[androidJvm]\ +val [queries](queries.md): [FirebaseDataConnect.Queries](-queries/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-ref/index.md new file mode 100644 index 00000000000..b19425f7bf4 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-ref/index.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[GetPostQueryRef](index.md) + +# GetPostQueryRef + +[androidJvm]\ +typealias [GetPostQueryRef](index.md) = [QueryRef](../-query-ref/index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-subscription/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-subscription/index.md new file mode 100644 index 00000000000..6474c7b7267 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-subscription/index.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[GetPostQuerySubscription](index.md) + +# GetPostQuerySubscription + +[androidJvm]\ +typealias [GetPostQuerySubscription](index.md) = [QuerySubscription](../-query-subscription/index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/index.md new file mode 100644 index 00000000000..e9c08b7ebb3 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/index.md @@ -0,0 +1,12 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Companion](index.md) + +# Companion + +[androidJvm]\ +object [Companion](index.md) + +## Functions + +| Name | Summary | +|---|---| +| [query](query.md) | [androidJvm]
fun [query](query.md)(dataConnect: [FirebaseDataConnect](../../-firebase-data-connect/index.md)): [QueryRef](../../-query-ref/index.md)<[GetPostQuery.Variables](../-variables/index.md), [GetPostQuery.Result](../-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/query.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/query.md new file mode 100644 index 00000000000..0b198fea4df --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/query.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Companion](index.md)/[query](query.md) + +# query + +[androidJvm]\ +fun [query](query.md)(dataConnect: [FirebaseDataConnect](../../-firebase-data-connect/index.md)): [QueryRef](../../-query-ref/index.md)<[GetPostQuery.Variables](../-variables/index.md), [GetPostQuery.Result](../-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/-comment.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/-comment.md new file mode 100644 index 00000000000..f6166547ca1 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/-comment.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[Comment](-comment.md) + +# Comment + +[androidJvm]\ +constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/content.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/content.md new file mode 100644 index 00000000000..bead6b5682e --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/content.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[content](content.md) + +# content + +[androidJvm]\ +val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/id.md new file mode 100644 index 00000000000..21255336892 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/id.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[id](id.md) + +# id + +[androidJvm]\ +val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/index.md new file mode 100644 index 00000000000..6084454e818 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/index.md @@ -0,0 +1,19 @@ +//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md) + +# Comment + +[androidJvm]\ +data class [Comment](index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) + +## Constructors + +| | | +|---|---| +| [Comment](-comment.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | + +## Properties + +| Name | Summary | +|---|---| +| [content](content.md) | [androidJvm]
val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [id](id.md) | [androidJvm]
val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-post.md new file mode 100644 index 00000000000..4b850ea32da --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-post.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[Post](-post.md) + +# Post + +[androidJvm]\ +constructor(content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/comments.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/comments.md new file mode 100644 index 00000000000..404f4097351 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/comments.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[comments](comments.md) + +# comments + +[androidJvm]\ +val [comments](comments.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/content.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/content.md new file mode 100644 index 00000000000..f1eb14144b0 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/content.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[content](content.md) + +# content + +[androidJvm]\ +val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/index.md new file mode 100644 index 00000000000..cbb3f7cd8ac --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/index.md @@ -0,0 +1,25 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md) + +# Post + +[androidJvm]\ +data class [Post](index.md)(val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) + +## Constructors + +| | | +|---|---| +| [Post](-post.md) | [androidJvm]
constructor(content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) | + +## Types + +| Name | Summary | +|---|---| +| [Comment](-comment/index.md) | [androidJvm]
data class [Comment](-comment/index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | + +## Properties + +| Name | Summary | +|---|---| +| [comments](comments.md) | [androidJvm]
val [comments](comments.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)> | +| [content](content.md) | [androidJvm]
val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-result.md new file mode 100644 index 00000000000..ec4a63a3023 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-result.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md)/[Result](-result.md) + +# Result + +[androidJvm]\ +constructor(post: [GetPostQuery.Result.Post](-post/index.md)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/index.md new file mode 100644 index 00000000000..655d32fb10a --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/index.md @@ -0,0 +1,24 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md) + +# Result + +[androidJvm]\ +data class [Result](index.md)(val post: [GetPostQuery.Result.Post](-post/index.md)) + +## Constructors + +| | | +|---|---| +| [Result](-result.md) | [androidJvm]
constructor(post: [GetPostQuery.Result.Post](-post/index.md)) | + +## Types + +| Name | Summary | +|---|---| +| [Post](-post/index.md) | [androidJvm]
data class [Post](-post/index.md)(val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-post/-comment/index.md)>) | + +## Properties + +| Name | Summary | +|---|---| +| [post](post.md) | [androidJvm]
val [post](post.md): [GetPostQuery.Result.Post](-post/index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/post.md new file mode 100644 index 00000000000..c557b5cd578 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/post.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md)/[post](post.md) + +# post + +[androidJvm]\ +val [post](post.md): [GetPostQuery.Result.Post](-post/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/-variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/-variables.md new file mode 100644 index 00000000000..f17d267a735 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/-variables.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[Variables](-variables.md) + +# Variables + +[androidJvm]\ +constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/id.md new file mode 100644 index 00000000000..b551b6258be --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/id.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[id](id.md) + +# id + +[androidJvm]\ +val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/index.md new file mode 100644 index 00000000000..59408401fe4 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/index.md @@ -0,0 +1,18 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md) + +# Variables + +[androidJvm]\ +data class [Variables](index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) + +## Constructors + +| | | +|---|---| +| [Variables](-variables.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | + +## Properties + +| Name | Summary | +|---|---| +| [id](id.md) | [androidJvm]
val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/index.md new file mode 100644 index 00000000000..720e76ae486 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/index.md @@ -0,0 +1,14 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[GetPostQuery](index.md) + +# GetPostQuery + +[androidJvm]\ +class [GetPostQuery](index.md) + +## Types + +| Name | Summary | +|---|---| +| [Companion](-companion/index.md) | [androidJvm]
object [Companion](-companion/index.md) | +| [Result](-result/index.md) | [androidJvm]
data class [Result](-result/index.md)(val post: [GetPostQuery.Result.Post](-result/-post/index.md)) | +| [Variables](-variables/index.md) | [androidJvm]
data class [Variables](-variables/index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-network-transport-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-network-transport-exception/index.md new file mode 100644 index 00000000000..5fb7ed09dfb --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-network-transport-exception/index.md @@ -0,0 +1,26 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[NetworkTransportException](index.md) + +# NetworkTransportException + +[androidJvm]\ +open class [NetworkTransportException](index.md) : [DataConnectException](../-data-connect-exception/index.md) + +## Properties + +| Name | Summary | +|---|---| +| [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | +| [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | + +## Functions + +| Name | Summary | +|---|---| +| [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | +| [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | +| [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | +| [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | +| [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/-query-ref.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/-query-ref.md new file mode 100644 index 00000000000..f721c1410bd --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/-query-ref.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QueryRef](index.md)/[QueryRef](-query-ref.md) + +# QueryRef + +[androidJvm]\ +constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/execute.md new file mode 100644 index 00000000000..36699e66c39 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/execute.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QueryRef](index.md)/[execute](execute.md) + +# execute + +[androidJvm]\ +open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/index.md new file mode 100644 index 00000000000..fc782790e26 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/index.md @@ -0,0 +1,27 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QueryRef](index.md) + +# QueryRef + +[androidJvm]\ +class [QueryRef](index.md)<[VariablesType](index.md), [ResultType](index.md)>(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) : [BaseRef](../-base-ref/index.md)<[VariablesType](index.md), [ResultType](index.md)> + +## Constructors + +| | | +|---|---| +| [QueryRef](-query-ref.md) | [androidJvm]
constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) | + +## Properties + +| Name | Summary | +|---|---| +| [dataConnect](../-base-ref/data-connect.md) | [androidJvm]
val [dataConnect](../-base-ref/data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) | + +## Functions + +| Name | Summary | +|---|---| +| [execute](execute.md) | [androidJvm]
open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) | +| [execute](../execute.md) | [androidJvm]
suspend fun [QueryRef](index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)>.[execute](../execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](../-get-post-query/-result/index.md) | +| [subscribe](subscribe.md) | [androidJvm]
fun [subscribe](subscribe.md)(variables: [VariablesType](index.md)): [QuerySubscription](../-query-subscription/index.md)<[VariablesType](index.md), [ResultType](index.md)> | +| [subscribe](../subscribe.md) | [androidJvm]
fun [QueryRef](index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)>.[subscribe](../subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](../-query-subscription/index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/subscribe.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/subscribe.md new file mode 100644 index 00000000000..96fa8b4a7be --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/subscribe.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QueryRef](index.md)/[subscribe](subscribe.md) + +# subscribe + +[androidJvm]\ +fun [subscribe](subscribe.md)(variables: [VariablesType](index.md)): [QuerySubscription](../-query-subscription/index.md)<[VariablesType](index.md), [ResultType](index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/flow.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/flow.md new file mode 100644 index 00000000000..17282ea3507 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/flow.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md)/[flow](flow.md) + +# flow + +[androidJvm]\ +val [flow](flow.md): Flow<[Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/index.md new file mode 100644 index 00000000000..aad5e137ebe --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/index.md @@ -0,0 +1,21 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md) + +# QuerySubscription + +[androidJvm]\ +class [QuerySubscription](index.md)<[VariablesType](index.md), [ResultType](index.md)> + +## Properties + +| Name | Summary | +|---|---| +| [flow](flow.md) | [androidJvm]
val [flow](flow.md): Flow<[Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>> | +| [lastResult](last-result.md) | [androidJvm]
val [lastResult](last-result.md): [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>? | +| [query](query.md) | [androidJvm]
val [query](query.md): [QueryRef](../-query-ref/index.md)<[VariablesType](index.md), [ResultType](index.md)> | +| [variables](variables.md) | [androidJvm]
val [variables](variables.md): [VariablesType](index.md) | + +## Functions + +| Name | Summary | +|---|---| +| [reload](reload.md) | [androidJvm]
fun [reload](reload.md)() | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/last-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/last-result.md new file mode 100644 index 00000000000..b807559e0c7 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/last-result.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md)/[lastResult](last-result.md) + +# lastResult + +[androidJvm]\ +val [lastResult](last-result.md): [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>? diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/query.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/query.md new file mode 100644 index 00000000000..6ef19668a8a --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/query.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md)/[query](query.md) + +# query + +[androidJvm]\ +val [query](query.md): [QueryRef](../-query-ref/index.md)<[VariablesType](index.md), [ResultType](index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/reload.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/reload.md new file mode 100644 index 00000000000..88b741acda2 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/reload.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md)/[reload](reload.md) + +# reload + +[androidJvm]\ +fun [reload](reload.md)() diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/variables.md new file mode 100644 index 00000000000..aada19de289 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/variables.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md)/[variables](variables.md) + +# variables + +[androidJvm]\ +val [variables](variables.md): [VariablesType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md new file mode 100644 index 00000000000..5681bc38bc6 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md @@ -0,0 +1,26 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[ResultDecodeException](index.md) + +# ResultDecodeException + +[androidJvm]\ +open class [ResultDecodeException](index.md) : [DataConnectException](../-data-connect-exception/index.md) + +## Properties + +| Name | Summary | +|---|---| +| [cause](index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | +| [message](index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | + +## Functions + +| Name | Summary | +|---|---| +| [addSuppressed](index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | +| [fillInStackTrace](index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [getLocalizedMessage](index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [getStackTrace](index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | +| [getSuppressed](index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | +| [initCause](index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [printStackTrace](index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | +| [setStackTrace](index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/execute.md new file mode 100644 index 00000000000..ff24d732e34 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/execute.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.apiproposal](index.md)/[execute](execute.md) + +# execute + +[androidJvm]\ +suspend fun [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](-get-post-query/-result/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/get-post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/get-post.md new file mode 100644 index 00000000000..a1c0add3c84 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/get-post.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.apiproposal](index.md)/[getPost](get-post.md) + +# getPost + +[androidJvm]\ +val [FirebaseDataConnect.Queries](-firebase-data-connect/-queries/index.md).[getPost](get-post.md): [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md new file mode 100644 index 00000000000..282a8e624c9 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md @@ -0,0 +1,32 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.apiproposal](index.md) + +# Package-level declarations + +## Types + +| Name | Summary | +|---|---| +| [BaseRef](-base-ref/index.md) | [androidJvm]
abstract class [BaseRef](-base-ref/index.md)<[VariablesType](-base-ref/index.md), [ResultType](-base-ref/index.md)> | +| [DataConnectException](-data-connect-exception/index.md) | [androidJvm]
open class [DataConnectException](-data-connect-exception/index.md) : [Exception](https://developer.android.com/reference/kotlin/java/lang/Exception.html) | +| [ExecutionException](-execution-exception/index.md) | [androidJvm]
open class [ExecutionException](-execution-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | +| [FirebaseDataConnect](-firebase-data-connect/index.md) | [androidJvm]
class [FirebaseDataConnect](-firebase-data-connect/index.md) | +| [GetPostQuery](-get-post-query/index.md) | [androidJvm]
class [GetPostQuery](-get-post-query/index.md) | +| [GetPostQueryRef](-get-post-query-ref/index.md) | [androidJvm]
typealias [GetPostQueryRef](-get-post-query-ref/index.md) = [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | +| [GetPostQuerySubscription](-get-post-query-subscription/index.md) | [androidJvm]
typealias [GetPostQuerySubscription](-get-post-query-subscription/index.md) = [QuerySubscription](-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | +| [NetworkTransportException](-network-transport-exception/index.md) | [androidJvm]
open class [NetworkTransportException](-network-transport-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | +| [QueryRef](-query-ref/index.md) | [androidJvm]
class [QueryRef](-query-ref/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)>(dataConnect: [FirebaseDataConnect](-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](-base-ref/-codec/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)>) : [BaseRef](-base-ref/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)> | +| [QuerySubscription](-query-subscription/index.md) | [androidJvm]
class [QuerySubscription](-query-subscription/index.md)<[VariablesType](-query-subscription/index.md), [ResultType](-query-subscription/index.md)> | +| [ResultDecodeException](-result-decode-exception/index.md) | [androidJvm]
open class [ResultDecodeException](-result-decode-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | + +## Properties + +| Name | Summary | +|---|---| +| [getPost](get-post.md) | [androidJvm]
val [FirebaseDataConnect.Queries](-firebase-data-connect/-queries/index.md).[getPost](get-post.md): [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | + +## Functions + +| Name | Summary | +|---|---| +| [execute](execute.md) | [androidJvm]
suspend fun [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](-get-post-query/-result/index.md) | +| [subscribe](subscribe.md) | [androidJvm]
fun [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[subscribe](subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/subscribe.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/subscribe.md new file mode 100644 index 00000000000..7ecca5478a8 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/subscribe.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.apiproposal](index.md)/[subscribe](subscribe.md) + +# subscribe + +[androidJvm]\ +fun [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[subscribe](subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/index.md new file mode 100644 index 00000000000..f060f0a67ab --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/index.md @@ -0,0 +1,12 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[CreatePostMutation](../index.md)/[Companion](index.md) + +# Companion + +[androidJvm]\ +object [Companion](index.md) + +## Functions + +| Name | Summary | +|---|---| +| [mutation](mutation.md) | [androidJvm]
fun [mutation](mutation.md)(dataConnect: [FirebaseDataConnect](../../../com.google.firebase.dataconnect/-firebase-data-connect/index.md)): [MutationRef](../../../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](../-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/mutation.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/mutation.md new file mode 100644 index 00000000000..98797225697 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/mutation.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[CreatePostMutation](../index.md)/[Companion](index.md)/[mutation](mutation.md) + +# mutation + +[androidJvm]\ +fun [mutation](mutation.md)(dataConnect: [FirebaseDataConnect](../../../com.google.firebase.dataconnect/-firebase-data-connect/index.md)): [MutationRef](../../../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](../-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/-post-data.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/-post-data.md new file mode 100644 index 00000000000..aec589f281b --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/-post-data.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[CreatePostMutation](../../index.md)/[Variables](../index.md)/[PostData](index.md)/[PostData](-post-data.md) + +# PostData + +[androidJvm]\ +constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/content.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/content.md new file mode 100644 index 00000000000..6b2aa04a7a3 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/content.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[CreatePostMutation](../../index.md)/[Variables](../index.md)/[PostData](index.md)/[content](content.md) + +# content + +[androidJvm]\ +val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/id.md new file mode 100644 index 00000000000..65a108b9366 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/id.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[CreatePostMutation](../../index.md)/[Variables](../index.md)/[PostData](index.md)/[id](id.md) + +# id + +[androidJvm]\ +val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/index.md new file mode 100644 index 00000000000..baf513ae85f --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/index.md @@ -0,0 +1,19 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[CreatePostMutation](../../index.md)/[Variables](../index.md)/[PostData](index.md) + +# PostData + +[androidJvm]\ +data class [PostData](index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) + +## Constructors + +| | | +|---|---| +| [PostData](-post-data.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | + +## Properties + +| Name | Summary | +|---|---| +| [content](content.md) | [androidJvm]
val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [id](id.md) | [androidJvm]
val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-variables.md new file mode 100644 index 00000000000..ee7b77d437f --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-variables.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[CreatePostMutation](../index.md)/[Variables](index.md)/[Variables](-variables.md) + +# Variables + +[androidJvm]\ +constructor(data: [CreatePostMutation.Variables.PostData](-post-data/index.md)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/data.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/data.md new file mode 100644 index 00000000000..601fade12b4 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/data.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[CreatePostMutation](../index.md)/[Variables](index.md)/[data](data.md) + +# data + +[androidJvm]\ +val [data](data.md): [CreatePostMutation.Variables.PostData](-post-data/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md new file mode 100644 index 00000000000..9120d5def22 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md @@ -0,0 +1,24 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[CreatePostMutation](../index.md)/[Variables](index.md) + +# Variables + +[androidJvm]\ +data class [Variables](index.md)(val data: [CreatePostMutation.Variables.PostData](-post-data/index.md)) + +## Constructors + +| | | +|---|---| +| [Variables](-variables.md) | [androidJvm]
constructor(data: [CreatePostMutation.Variables.PostData](-post-data/index.md)) | + +## Types + +| Name | Summary | +|---|---| +| [PostData](-post-data/index.md) | [androidJvm]
data class [PostData](-post-data/index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | + +## Properties + +| Name | Summary | +|---|---| +| [data](data.md) | [androidJvm]
val [data](data.md): [CreatePostMutation.Variables.PostData](-post-data/index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/index.md new file mode 100644 index 00000000000..a6d504d95ac --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/index.md @@ -0,0 +1,13 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.generated](../index.md)/[CreatePostMutation](index.md) + +# CreatePostMutation + +[androidJvm]\ +class [CreatePostMutation](index.md) + +## Types + +| Name | Summary | +|---|---| +| [Companion](-companion/index.md) | [androidJvm]
object [Companion](-companion/index.md) | +| [Variables](-variables/index.md) | [androidJvm]
data class [Variables](-variables/index.md)(val data: [CreatePostMutation.Variables.PostData](-variables/-post-data/index.md)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query-subscription/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query-subscription/index.md new file mode 100644 index 00000000000..b858484ab7b --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query-subscription/index.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.generated](../index.md)/[GetPostQuerySubscription](index.md) + +# GetPostQuerySubscription + +[androidJvm]\ +typealias [GetPostQuerySubscription](index.md) = [QuerySubscription](../../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/index.md new file mode 100644 index 00000000000..386d1959656 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/index.md @@ -0,0 +1,12 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Companion](index.md) + +# Companion + +[androidJvm]\ +object [Companion](index.md) + +## Functions + +| Name | Summary | +|---|---| +| [query](query.md) | [androidJvm]
fun [query](query.md)(dataConnect: [FirebaseDataConnect](../../../com.google.firebase.dataconnect/-firebase-data-connect/index.md)): [QueryRef](../../../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](../-variables/index.md), [GetPostQuery.Result](../-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/query.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/query.md new file mode 100644 index 00000000000..e01a0937810 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/query.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Companion](index.md)/[query](query.md) + +# query + +[androidJvm]\ +fun [query](query.md)(dataConnect: [FirebaseDataConnect](../../../com.google.firebase.dataconnect/-firebase-data-connect/index.md)): [QueryRef](../../../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](../-variables/index.md), [GetPostQuery.Result](../-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/-comment.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/-comment.md new file mode 100644 index 00000000000..7853de8c678 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/-comment.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[Comment](-comment.md) + +# Comment + +[androidJvm]\ +constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/content.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/content.md new file mode 100644 index 00000000000..b1e4570df0e --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/content.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[content](content.md) + +# content + +[androidJvm]\ +val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/id.md new file mode 100644 index 00000000000..ff063410da2 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/id.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[id](id.md) + +# id + +[androidJvm]\ +val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/index.md new file mode 100644 index 00000000000..1f2f7c8bcd4 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/index.md @@ -0,0 +1,19 @@ +//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md) + +# Comment + +[androidJvm]\ +data class [Comment](index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) + +## Constructors + +| | | +|---|---| +| [Comment](-comment.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | + +## Properties + +| Name | Summary | +|---|---| +| [content](content.md) | [androidJvm]
val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [id](id.md) | [androidJvm]
val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-post.md new file mode 100644 index 00000000000..917b60a352a --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-post.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[Post](-post.md) + +# Post + +[androidJvm]\ +constructor(content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/comments.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/comments.md new file mode 100644 index 00000000000..b1f5d4e2968 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/comments.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[comments](comments.md) + +# comments + +[androidJvm]\ +val [comments](comments.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/content.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/content.md new file mode 100644 index 00000000000..f09ff4af0ce --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/content.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[content](content.md) + +# content + +[androidJvm]\ +val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/index.md new file mode 100644 index 00000000000..f375dff63ee --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/index.md @@ -0,0 +1,25 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md) + +# Post + +[androidJvm]\ +data class [Post](index.md)(val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) + +## Constructors + +| | | +|---|---| +| [Post](-post.md) | [androidJvm]
constructor(content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) | + +## Types + +| Name | Summary | +|---|---| +| [Comment](-comment/index.md) | [androidJvm]
data class [Comment](-comment/index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | + +## Properties + +| Name | Summary | +|---|---| +| [comments](comments.md) | [androidJvm]
val [comments](comments.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)> | +| [content](content.md) | [androidJvm]
val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-result.md new file mode 100644 index 00000000000..4f9b1ebcf4d --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-result.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md)/[Result](-result.md) + +# Result + +[androidJvm]\ +constructor(post: [GetPostQuery.Result.Post](-post/index.md)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md new file mode 100644 index 00000000000..ff0627369cc --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md @@ -0,0 +1,24 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md) + +# Result + +[androidJvm]\ +data class [Result](index.md)(val post: [GetPostQuery.Result.Post](-post/index.md)) + +## Constructors + +| | | +|---|---| +| [Result](-result.md) | [androidJvm]
constructor(post: [GetPostQuery.Result.Post](-post/index.md)) | + +## Types + +| Name | Summary | +|---|---| +| [Post](-post/index.md) | [androidJvm]
data class [Post](-post/index.md)(val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-post/-comment/index.md)>) | + +## Properties + +| Name | Summary | +|---|---| +| [post](post.md) | [androidJvm]
val [post](post.md): [GetPostQuery.Result.Post](-post/index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/post.md new file mode 100644 index 00000000000..5c0473c0a8a --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/post.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md)/[post](post.md) + +# post + +[androidJvm]\ +val [post](post.md): [GetPostQuery.Result.Post](-post/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/-builder.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/-builder.md new file mode 100644 index 00000000000..028149df152 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/-builder.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Variables](../index.md)/[Builder](index.md)/[Builder](-builder.md) + +# Builder + +[androidJvm]\ +constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/build.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/build.md new file mode 100644 index 00000000000..1491e88aa66 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/build.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Variables](../index.md)/[Builder](index.md)/[build](build.md) + +# build + +[androidJvm]\ +fun [build](build.md)(): [GetPostQuery.Variables](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/id.md new file mode 100644 index 00000000000..f7e8c9a6527 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/id.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Variables](../index.md)/[Builder](index.md)/[id](id.md) + +# id + +[androidJvm]\ +var [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md new file mode 100644 index 00000000000..105626cc0bd --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md @@ -0,0 +1,24 @@ +//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Variables](../index.md)/[Builder](index.md) + +# Builder + +[androidJvm]\ +class [Builder](index.md)(var id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) + +## Constructors + +| | | +|---|---| +| [Builder](-builder.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | + +## Properties + +| Name | Summary | +|---|---| +| [id](id.md) | [androidJvm]
var [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | + +## Functions + +| Name | Summary | +|---|---| +| [build](build.md) | [androidJvm]
fun [build](build.md)(): [GetPostQuery.Variables](../index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-variables.md new file mode 100644 index 00000000000..c67ae8feaea --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-variables.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[Variables](-variables.md) + +# Variables + +[androidJvm]\ +constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/build.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/build.md new file mode 100644 index 00000000000..4cf69611ea1 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/build.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[build](build.md) + +# build + +[androidJvm]\ +fun [build](build.md)(block: [GetPostQuery.Variables.Builder](-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)): [GetPostQuery.Variables](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/builder.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/builder.md new file mode 100644 index 00000000000..f36b3df7a56 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/builder.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[builder](builder.md) + +# builder + +[androidJvm]\ +val [builder](builder.md): [GetPostQuery.Variables.Builder](-builder/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/id.md new file mode 100644 index 00000000000..cefd68ad494 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/id.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[id](id.md) + +# id + +[androidJvm]\ +val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md new file mode 100644 index 00000000000..b2876271cc3 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md @@ -0,0 +1,31 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md) + +# Variables + +[androidJvm]\ +data class [Variables](index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) + +## Constructors + +| | | +|---|---| +| [Variables](-variables.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | + +## Types + +| Name | Summary | +|---|---| +| [Builder](-builder/index.md) | [androidJvm]
class [Builder](-builder/index.md)(var id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | + +## Properties + +| Name | Summary | +|---|---| +| [builder](builder.md) | [androidJvm]
val [builder](builder.md): [GetPostQuery.Variables.Builder](-builder/index.md) | +| [id](id.md) | [androidJvm]
val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | + +## Functions + +| Name | Summary | +|---|---| +| [build](build.md) | [androidJvm]
fun [build](build.md)(block: [GetPostQuery.Variables.Builder](-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)): [GetPostQuery.Variables](index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/index.md new file mode 100644 index 00000000000..c9fc41fce8f --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/index.md @@ -0,0 +1,14 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.generated](../index.md)/[GetPostQuery](index.md) + +# GetPostQuery + +[androidJvm]\ +class [GetPostQuery](index.md) + +## Types + +| Name | Summary | +|---|---| +| [Companion](-companion/index.md) | [androidJvm]
object [Companion](-companion/index.md) | +| [Result](-result/index.md) | [androidJvm]
data class [Result](-result/index.md)(val post: [GetPostQuery.Result.Post](-result/-post/index.md)) | +| [Variables](-variables/index.md) | [androidJvm]
data class [Variables](-variables/index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/create-post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/create-post.md new file mode 100644 index 00000000000..7b0f285eedf --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/create-post.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md)/[createPost](create-post.md) + +# createPost + +[androidJvm]\ +val [FirebaseDataConnect.Mutations](../com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md).[createPost](create-post.md): [MutationRef](../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md new file mode 100644 index 00000000000..476dec921ed --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md @@ -0,0 +1,8 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md)/[execute](execute.md) + +# execute + +[androidJvm]\ +suspend fun [MutationRef](../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) + +suspend fun [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](-get-post-query/-result/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/get-post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/get-post.md new file mode 100644 index 00000000000..54d2d69b7f9 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/get-post.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md)/[getPost](get-post.md) + +# getPost + +[androidJvm]\ +val [FirebaseDataConnect.Queries](../com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md).[getPost](get-post.md): [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md new file mode 100644 index 00000000000..4768ecb4b5c --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md @@ -0,0 +1,26 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md) + +# Package-level declarations + +## Types + +| Name | Summary | +|---|---| +| [CreatePostMutation](-create-post-mutation/index.md) | [androidJvm]
class [CreatePostMutation](-create-post-mutation/index.md) | +| [GetPostQuery](-get-post-query/index.md) | [androidJvm]
class [GetPostQuery](-get-post-query/index.md) | +| [GetPostQuerySubscription](-get-post-query-subscription/index.md) | [androidJvm]
typealias [GetPostQuerySubscription](-get-post-query-subscription/index.md) = [QuerySubscription](../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | + +## Properties + +| Name | Summary | +|---|---| +| [createPost](create-post.md) | [androidJvm]
val [FirebaseDataConnect.Mutations](../com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md).[createPost](create-post.md): [MutationRef](../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)> | +| [getPost](get-post.md) | [androidJvm]
val [FirebaseDataConnect.Queries](../com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md).[getPost](get-post.md): [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | + +## Functions + +| Name | Summary | +|---|---| +| [execute](execute.md) | [androidJvm]
suspend fun [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](-get-post-query/-result/index.md)
suspend fun [MutationRef](../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | +| [subscribe](subscribe.md) | [androidJvm]
fun [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[subscribe](subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | +| [update](update.md) | [androidJvm]
fun [QuerySubscription](../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[update](update.md)(block: [GetPostQuery.Variables.Builder](-get-post-query/-variables/-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/subscribe.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/subscribe.md new file mode 100644 index 00000000000..e3e8718dda7 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/subscribe.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md)/[subscribe](subscribe.md) + +# subscribe + +[androidJvm]\ +fun [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[subscribe](subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/update.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/update.md new file mode 100644 index 00000000000..9b13324b0ab --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/update.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md)/[update](update.md) + +# update + +[androidJvm]\ +fun [QuerySubscription](../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[update](update.md)(block: [GetPostQuery.Variables.Builder](-get-post-query/-variables/-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/decode-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/decode-result.md new file mode 100644 index 00000000000..1e6826ca0e2 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/decode-result.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[BaseRef](../index.md)/[Codec](index.md)/[decodeResult](decode-result.md) + +# decodeResult + +[androidJvm]\ +abstract fun [decodeResult](decode-result.md)(map: [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?>): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/encode-variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/encode-variables.md new file mode 100644 index 00000000000..d5c1d9cf534 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/encode-variables.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[BaseRef](../index.md)/[Codec](index.md)/[encodeVariables](encode-variables.md) + +# encodeVariables + +[androidJvm]\ +abstract fun [encodeVariables](encode-variables.md)(variables: [VariablesType](index.md)): [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/index.md new file mode 100644 index 00000000000..bbe2ea734f0 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/index.md @@ -0,0 +1,13 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[BaseRef](../index.md)/[Codec](index.md) + +# Codec + +[androidJvm]\ +interface [Codec](index.md)<[VariablesType](index.md), [ResultType](index.md)> + +## Functions + +| Name | Summary | +|---|---| +| [decodeResult](decode-result.md) | [androidJvm]
abstract fun [decodeResult](decode-result.md)(map: [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?>): [ResultType](index.md) | +| [encodeVariables](encode-variables.md) | [androidJvm]
abstract fun [encodeVariables](encode-variables.md)(variables: [VariablesType](index.md)): [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/data-connect.md new file mode 100644 index 00000000000..3d37fb90aab --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/data-connect.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[BaseRef](index.md)/[dataConnect](data-connect.md) + +# dataConnect + +[androidJvm]\ +val [dataConnect](data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/execute.md new file mode 100644 index 00000000000..c73926c34e2 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/execute.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[BaseRef](index.md)/[execute](execute.md) + +# execute + +[androidJvm]\ +abstract suspend fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/index.md new file mode 100644 index 00000000000..03e1a146a8e --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/index.md @@ -0,0 +1,30 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[BaseRef](index.md) + +# BaseRef + +abstract class [BaseRef](index.md)<[VariablesType](index.md), [ResultType](index.md)> + +#### Inheritors + +| | +|---| +| [MutationRef](../-mutation-ref/index.md) | +| [QueryRef](../-query-ref/index.md) | + +## Types + +| Name | Summary | +|---|---| +| [Codec](-codec/index.md) | [androidJvm]
interface [Codec](-codec/index.md)<[VariablesType](-codec/index.md), [ResultType](-codec/index.md)> | + +## Properties + +| Name | Summary | +|---|---| +| [dataConnect](data-connect.md) | [androidJvm]
val [dataConnect](data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) | + +## Functions + +| Name | Summary | +|---|---| +| [execute](execute.md) | [androidJvm]
abstract suspend fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-data-connect-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-data-connect-exception/index.md new file mode 100644 index 00000000000..3fc9a1dd3c7 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-data-connect-exception/index.md @@ -0,0 +1,33 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[DataConnectException](index.md) + +# DataConnectException + +open class [DataConnectException](index.md) : [Exception](https://developer.android.com/reference/kotlin/java/lang/Exception.html) + +#### Inheritors + +| | +|---| +| [NetworkTransportException](../-network-transport-exception/index.md) | +| [GraphQLException](../-graph-q-l-exception/index.md) | +| [ResultDecodeException](../-result-decode-exception/index.md) | + +## Properties + +| Name | Summary | +|---|---| +| [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | +| [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | + +## Functions + +| Name | Summary | +|---|---| +| [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | +| [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | +| [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | +| [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | +| [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md new file mode 100644 index 00000000000..387b2854203 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md @@ -0,0 +1,8 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md)/[build](build.md) + +# build + +[androidJvm]\ +fun [build](build.md)(): [FirebaseDataConnectSettings](../index.md) + +fun [build](build.md)(block: [FirebaseDataConnectSettings.Builder](index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)): [FirebaseDataConnectSettings](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/connect-to-emulator.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/connect-to-emulator.md new file mode 100644 index 00000000000..e76328becd6 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/connect-to-emulator.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md)/[connectToEmulator](connect-to-emulator.md) + +# connectToEmulator + +[androidJvm]\ +fun [connectToEmulator](connect-to-emulator.md)() diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/host-name.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/host-name.md new file mode 100644 index 00000000000..fbbae7811f5 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/host-name.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md)/[hostName](host-name.md) + +# hostName + +[androidJvm]\ +var [hostName](host-name.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/index.md new file mode 100644 index 00000000000..c778ca6c917 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/index.md @@ -0,0 +1,21 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md) + +# Builder + +[androidJvm]\ +class [Builder](index.md) + +## Properties + +| Name | Summary | +|---|---| +| [hostName](host-name.md) | [androidJvm]
var [hostName](host-name.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [port](port.md) | [androidJvm]
var [port](port.md): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | +| [sslEnabled](ssl-enabled.md) | [androidJvm]
var [sslEnabled](ssl-enabled.md): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) | + +## Functions + +| Name | Summary | +|---|---| +| [build](build.md) | [androidJvm]
fun [build](build.md)(): [FirebaseDataConnectSettings](../index.md)
fun [build](build.md)(block: [FirebaseDataConnectSettings.Builder](index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)): [FirebaseDataConnectSettings](../index.md) | +| [connectToEmulator](connect-to-emulator.md) | [androidJvm]
fun [connectToEmulator](connect-to-emulator.md)() | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/port.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/port.md new file mode 100644 index 00000000000..30c59011838 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/port.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md)/[port](port.md) + +# port + +[androidJvm]\ +var [port](port.md): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/ssl-enabled.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/ssl-enabled.md new file mode 100644 index 00000000000..d29bf91ab1b --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/ssl-enabled.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md)/[sslEnabled](ssl-enabled.md) + +# sslEnabled + +[androidJvm]\ +var [sslEnabled](ssl-enabled.md): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/defaults.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/defaults.md new file mode 100644 index 00000000000..f020b3fb29e --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/defaults.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Companion](index.md)/[defaults](defaults.md) + +# defaults + +[androidJvm]\ +val [defaults](defaults.md): [FirebaseDataConnectSettings](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/index.md new file mode 100644 index 00000000000..3ea913736be --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/index.md @@ -0,0 +1,12 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Companion](index.md) + +# Companion + +[androidJvm]\ +object [Companion](index.md) + +## Properties + +| Name | Summary | +|---|---| +| [defaults](defaults.md) | [androidJvm]
val [defaults](defaults.md): [FirebaseDataConnectSettings](../index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/builder.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/builder.md new file mode 100644 index 00000000000..ab802f17164 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/builder.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[builder](builder.md) + +# builder + +[androidJvm]\ +val [builder](builder.md): [FirebaseDataConnectSettings.Builder](-builder/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/equals.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/equals.md new file mode 100644 index 00000000000..df0e222aa3b --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/equals.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[equals](equals.md) + +# equals + +[androidJvm]\ +open operator override fun [equals](equals.md)(other: [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/hash-code.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/hash-code.md new file mode 100644 index 00000000000..486ca715356 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/hash-code.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[hashCode](hash-code.md) + +# hashCode + +[androidJvm]\ +open override fun [hashCode](hash-code.md)(): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/host-name.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/host-name.md new file mode 100644 index 00000000000..1c188421ea4 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/host-name.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[hostName](host-name.md) + +# hostName + +[androidJvm]\ +val [hostName](host-name.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/index.md new file mode 100644 index 00000000000..1ed330bf008 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/index.md @@ -0,0 +1,30 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md) + +# FirebaseDataConnectSettings + +[androidJvm]\ +class [FirebaseDataConnectSettings](index.md) + +## Types + +| Name | Summary | +|---|---| +| [Builder](-builder/index.md) | [androidJvm]
class [Builder](-builder/index.md) | +| [Companion](-companion/index.md) | [androidJvm]
object [Companion](-companion/index.md) | + +## Properties + +| Name | Summary | +|---|---| +| [builder](builder.md) | [androidJvm]
val [builder](builder.md): [FirebaseDataConnectSettings.Builder](-builder/index.md) | +| [hostName](host-name.md) | [androidJvm]
val [hostName](host-name.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [port](port.md) | [androidJvm]
val [port](port.md): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | +| [sslEnabled](ssl-enabled.md) | [androidJvm]
val [sslEnabled](ssl-enabled.md): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) | + +## Functions + +| Name | Summary | +|---|---| +| [equals](equals.md) | [androidJvm]
open operator override fun [equals](equals.md)(other: [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) | +| [hashCode](hash-code.md) | [androidJvm]
open override fun [hashCode](hash-code.md)(): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | +| [toString](to-string.md) | [androidJvm]
open override fun [toString](to-string.md)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/port.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/port.md new file mode 100644 index 00000000000..48f1bffe411 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/port.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[port](port.md) + +# port + +[androidJvm]\ +val [port](port.md): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/ssl-enabled.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/ssl-enabled.md new file mode 100644 index 00000000000..2cfed3000e3 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/ssl-enabled.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[sslEnabled](ssl-enabled.md) + +# sslEnabled + +[androidJvm]\ +val [sslEnabled](ssl-enabled.md): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/to-string.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/to-string.md new file mode 100644 index 00000000000..a99556f49c3 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/to-string.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[toString](to-string.md) + +# toString + +[androidJvm]\ +open override fun [toString](to-string.md)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md new file mode 100644 index 00000000000..06c3bfaf12d --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md @@ -0,0 +1,8 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Companion](index.md)/[getInstance](get-instance.md) + +# getInstance + +[androidJvm]\ +fun [getInstance](get-instance.md)(location: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), service: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [FirebaseDataConnect](../index.md) + +fun [getInstance](get-instance.md)(app: FirebaseApp, location: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), service: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [FirebaseDataConnect](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/index.md new file mode 100644 index 00000000000..feabf7fb6b7 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/index.md @@ -0,0 +1,12 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Companion](index.md) + +# Companion + +[androidJvm]\ +object [Companion](index.md) + +## Functions + +| Name | Summary | +|---|---| +| [getInstance](get-instance.md) | [androidJvm]
fun [getInstance](get-instance.md)(location: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), service: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [FirebaseDataConnect](../index.md)
fun [getInstance](get-instance.md)(app: FirebaseApp, location: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), service: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [FirebaseDataConnect](../index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/data-connect.md new file mode 100644 index 00000000000..da50daa6446 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/data-connect.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Mutations](index.md)/[dataConnect](data-connect.md) + +# dataConnect + +[androidJvm]\ +val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md new file mode 100644 index 00000000000..03f8fb66526 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md @@ -0,0 +1,13 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Mutations](index.md) + +# Mutations + +[androidJvm]\ +class [Mutations](index.md) + +## Properties + +| Name | Summary | +|---|---| +| [createPost](../../../com.google.firebase.dataconnect.generated/create-post.md) | [androidJvm]
val [FirebaseDataConnect.Mutations](index.md).[createPost](../../../com.google.firebase.dataconnect.generated/create-post.md): [MutationRef](../../-mutation-ref/index.md)<[CreatePostMutation.Variables](../../../com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)> | +| [dataConnect](data-connect.md) | [androidJvm]
val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/data-connect.md new file mode 100644 index 00000000000..8666ae0eb1e --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/data-connect.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Queries](index.md)/[dataConnect](data-connect.md) + +# dataConnect + +[androidJvm]\ +val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md new file mode 100644 index 00000000000..f70f3dbeb4e --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md @@ -0,0 +1,13 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Queries](index.md) + +# Queries + +[androidJvm]\ +class [Queries](index.md) + +## Properties + +| Name | Summary | +|---|---| +| [dataConnect](data-connect.md) | [androidJvm]
val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) | +| [getPost](../../../com.google.firebase.dataconnect.generated/get-post.md) | [androidJvm]
val [FirebaseDataConnect.Queries](index.md).[getPost](../../../com.google.firebase.dataconnect.generated/get-post.md): [QueryRef](../../-query-ref/index.md)<[GetPostQuery.Variables](../../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md), [GetPostQuery.Result](../../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/app.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/app.md new file mode 100644 index 00000000000..33880164cc1 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/app.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[app](app.md) + +# app + +[androidJvm]\ +val [app](app.md): FirebaseApp diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/close.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/close.md new file mode 100644 index 00000000000..ee2dba34c6b --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/close.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[close](close.md) + +# close + +[androidJvm]\ +open override fun [close](close.md)() diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/index.md new file mode 100644 index 00000000000..125525362f8 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/index.md @@ -0,0 +1,33 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md) + +# FirebaseDataConnect + +[androidJvm]\ +class [FirebaseDataConnect](index.md) : [Closeable](https://developer.android.com/reference/kotlin/java/io/Closeable.html) + +## Types + +| Name | Summary | +|---|---| +| [Companion](-companion/index.md) | [androidJvm]
object [Companion](-companion/index.md) | +| [Mutations](-mutations/index.md) | [androidJvm]
class [Mutations](-mutations/index.md) | +| [Queries](-queries/index.md) | [androidJvm]
class [Queries](-queries/index.md) | + +## Properties + +| Name | Summary | +|---|---| +| [app](app.md) | [androidJvm]
val [app](app.md): FirebaseApp | +| [location](location.md) | [androidJvm]
val [location](location.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [mutations](mutations.md) | [androidJvm]
val [mutations](mutations.md): [FirebaseDataConnect.Mutations](-mutations/index.md) | +| [queries](queries.md) | [androidJvm]
val [queries](queries.md): [FirebaseDataConnect.Queries](-queries/index.md) | +| [service](service.md) | [androidJvm]
val [service](service.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [settings](settings.md) | [androidJvm]
var [settings](settings.md): [FirebaseDataConnectSettings](../-firebase-data-connect-settings/index.md) | + +## Functions + +| Name | Summary | +|---|---| +| [close](close.md) | [androidJvm]
open override fun [close](close.md)() | +| [toString](to-string.md) | [androidJvm]
open override fun [toString](to-string.md)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [updateSettings](update-settings.md) | [androidJvm]
fun [updateSettings](update-settings.md)(block: [FirebaseDataConnectSettings.Builder](../-firebase-data-connect-settings/-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/location.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/location.md new file mode 100644 index 00000000000..7db18da669e --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/location.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[location](location.md) + +# location + +[androidJvm]\ +val [location](location.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/mutations.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/mutations.md new file mode 100644 index 00000000000..1b927867f39 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/mutations.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[mutations](mutations.md) + +# mutations + +[androidJvm]\ +val [mutations](mutations.md): [FirebaseDataConnect.Mutations](-mutations/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/queries.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/queries.md new file mode 100644 index 00000000000..8de482ce9bd --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/queries.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[queries](queries.md) + +# queries + +[androidJvm]\ +val [queries](queries.md): [FirebaseDataConnect.Queries](-queries/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/service.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/service.md new file mode 100644 index 00000000000..f7e86eb9730 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/service.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[service](service.md) + +# service + +[androidJvm]\ +val [service](service.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/settings.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/settings.md new file mode 100644 index 00000000000..c787fadc53f --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/settings.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[settings](settings.md) + +# settings + +[androidJvm]\ +var [settings](settings.md): [FirebaseDataConnectSettings](../-firebase-data-connect-settings/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/to-string.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/to-string.md new file mode 100644 index 00000000000..84ce9a4156f --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/to-string.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[toString](to-string.md) + +# toString + +[androidJvm]\ +open override fun [toString](to-string.md)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/update-settings.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/update-settings.md new file mode 100644 index 00000000000..39069967ae6 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/update-settings.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[updateSettings](update-settings.md) + +# updateSettings + +[androidJvm]\ +fun [updateSettings](update-settings.md)(block: [FirebaseDataConnectSettings.Builder](../-firebase-data-connect-settings/-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/errors.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/errors.md new file mode 100644 index 00000000000..de24560f2b4 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/errors.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[GraphQLException](index.md)/[errors](errors.md) + +# errors + +[androidJvm]\ +val [errors](errors.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/index.md new file mode 100644 index 00000000000..6097c9938b2 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/index.md @@ -0,0 +1,27 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[GraphQLException](index.md) + +# GraphQLException + +[androidJvm]\ +open class [GraphQLException](index.md) : [DataConnectException](../-data-connect-exception/index.md) + +## Properties + +| Name | Summary | +|---|---| +| [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | +| [errors](errors.md) | [androidJvm]
val [errors](errors.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)> | +| [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | + +## Functions + +| Name | Summary | +|---|---| +| [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | +| [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | +| [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | +| [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | +| [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-d-e-b-u-g/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-d-e-b-u-g/index.md new file mode 100644 index 00000000000..6a949ed9213 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-d-e-b-u-g/index.md @@ -0,0 +1,13 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[LogLevel](../index.md)/[DEBUG](index.md) + +# DEBUG + +[androidJvm]\ +[DEBUG](index.md) + +## Properties + +| Name | Summary | +|---|---| +| [name](../-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345) | [androidJvm]
val [name](../-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [ordinal](../-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345) | [androidJvm]
val [ordinal](../-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-i-n-f-o/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-i-n-f-o/index.md new file mode 100644 index 00000000000..94a378382aa --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-i-n-f-o/index.md @@ -0,0 +1,13 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[LogLevel](../index.md)/[INFO](index.md) + +# INFO + +[androidJvm]\ +[INFO](index.md) + +## Properties + +| Name | Summary | +|---|---| +| [name](../-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345) | [androidJvm]
val [name](../-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [ordinal](../-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345) | [androidJvm]
val [ordinal](../-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-w-a-r-n-i-n-g/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-w-a-r-n-i-n-g/index.md new file mode 100644 index 00000000000..8bbd47a76c6 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-w-a-r-n-i-n-g/index.md @@ -0,0 +1,13 @@ +//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[LogLevel](../index.md)/[WARNING](index.md) + +# WARNING + +[androidJvm]\ +[WARNING](index.md) + +## Properties + +| Name | Summary | +|---|---| +| [name](index.md#-372974862%2FProperties%2F1090735345) | [androidJvm]
val [name](index.md#-372974862%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [ordinal](index.md#-739389684%2FProperties%2F1090735345) | [androidJvm]
val [ordinal](index.md#-739389684%2FProperties%2F1090735345): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/entries.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/entries.md new file mode 100644 index 00000000000..9dc69f17c43 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/entries.md @@ -0,0 +1,10 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[LogLevel](index.md)/[entries](entries.md) + +# entries + +[androidJvm]\ +val [entries](entries.md): [EnumEntries](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.enums/-enum-entries/index.html)<[LogLevel](index.md)> + +Returns a representation of an immutable list of all enum entries, in the order they're declared. + +This method may be used to iterate over the enum entries. diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/index.md new file mode 100644 index 00000000000..1cb9167f6f7 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/index.md @@ -0,0 +1,29 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[LogLevel](index.md) + +# LogLevel + +[androidJvm]\ +enum [LogLevel](index.md) : [Enum](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-enum/index.html)<[LogLevel](index.md)> + +## Entries + +| | | +|---|---| +| [DEBUG](-d-e-b-u-g/index.md) | [androidJvm]
[DEBUG](-d-e-b-u-g/index.md) | +| [INFO](-i-n-f-o/index.md) | [androidJvm]
[INFO](-i-n-f-o/index.md) | +| [WARNING](-w-a-r-n-i-n-g/index.md) | [androidJvm]
[WARNING](-w-a-r-n-i-n-g/index.md) | + +## Properties + +| Name | Summary | +|---|---| +| [entries](entries.md) | [androidJvm]
val [entries](entries.md): [EnumEntries](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.enums/-enum-entries/index.html)<[LogLevel](index.md)>
Returns a representation of an immutable list of all enum entries, in the order they're declared. | +| [name](-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345) | [androidJvm]
val [name](-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [ordinal](-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345) | [androidJvm]
val [ordinal](-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | + +## Functions + +| Name | Summary | +|---|---| +| [valueOf](value-of.md) | [androidJvm]
fun [valueOf](value-of.md)(value: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [LogLevel](index.md)
Returns the enum constant of this type with the specified name. The string must match exactly an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.) | +| [values](values.md) | [androidJvm]
fun [values](values.md)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[LogLevel](index.md)>
Returns an array containing the constants of this enum type, in the order they're declared. | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/value-of.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/value-of.md new file mode 100644 index 00000000000..6f698edc2ae --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/value-of.md @@ -0,0 +1,14 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[LogLevel](index.md)/[valueOf](value-of.md) + +# valueOf + +[androidJvm]\ +fun [valueOf](value-of.md)(value: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [LogLevel](index.md) + +Returns the enum constant of this type with the specified name. The string must match exactly an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.) + +#### Throws + +| | | +|---|---| +| [IllegalArgumentException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-illegal-argument-exception/index.html) | if this enum type has no constant with the specified name | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/values.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/values.md new file mode 100644 index 00000000000..a35fab2c41f --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/values.md @@ -0,0 +1,10 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[LogLevel](index.md)/[values](values.md) + +# values + +[androidJvm]\ +fun [values](values.md)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[LogLevel](index.md)> + +Returns an array containing the constants of this enum type, in the order they're declared. + +This method may be used to iterate over the constants. diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/-mutation-ref.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/-mutation-ref.md new file mode 100644 index 00000000000..ab61452398f --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/-mutation-ref.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[MutationRef](index.md)/[MutationRef](-mutation-ref.md) + +# MutationRef + +[androidJvm]\ +constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/execute.md new file mode 100644 index 00000000000..c6565aeefed --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/execute.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[MutationRef](index.md)/[execute](execute.md) + +# execute + +[androidJvm]\ +open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/index.md new file mode 100644 index 00000000000..0a5edcdd9b4 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/index.md @@ -0,0 +1,25 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[MutationRef](index.md) + +# MutationRef + +[androidJvm]\ +class [MutationRef](index.md)<[VariablesType](index.md), [ResultType](index.md)>(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) : [BaseRef](../-base-ref/index.md)<[VariablesType](index.md), [ResultType](index.md)> + +## Constructors + +| | | +|---|---| +| [MutationRef](-mutation-ref.md) | [androidJvm]
constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) | + +## Properties + +| Name | Summary | +|---|---| +| [dataConnect](../-base-ref/data-connect.md) | [androidJvm]
val [dataConnect](../-base-ref/data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) | + +## Functions + +| Name | Summary | +|---|---| +| [execute](execute.md) | [androidJvm]
open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) | +| [execute](../../com.google.firebase.dataconnect.generated/execute.md) | [androidJvm]
suspend fun [MutationRef](index.md)<[CreatePostMutation.Variables](../../com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)>.[execute](../../com.google.firebase.dataconnect.generated/execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-network-transport-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-network-transport-exception/index.md new file mode 100644 index 00000000000..37692d7299c --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-network-transport-exception/index.md @@ -0,0 +1,26 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[NetworkTransportException](index.md) + +# NetworkTransportException + +[androidJvm]\ +open class [NetworkTransportException](index.md) : [DataConnectException](../-data-connect-exception/index.md) + +## Properties + +| Name | Summary | +|---|---| +| [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | +| [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | + +## Functions + +| Name | Summary | +|---|---| +| [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | +| [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | +| [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | +| [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | +| [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/-query-ref.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/-query-ref.md new file mode 100644 index 00000000000..b0f776b2d10 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/-query-ref.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QueryRef](index.md)/[QueryRef](-query-ref.md) + +# QueryRef + +[androidJvm]\ +constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/execute.md new file mode 100644 index 00000000000..7df3894ff6d --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/execute.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QueryRef](index.md)/[execute](execute.md) + +# execute + +[androidJvm]\ +open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/index.md new file mode 100644 index 00000000000..e4cd167b23c --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/index.md @@ -0,0 +1,27 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QueryRef](index.md) + +# QueryRef + +[androidJvm]\ +class [QueryRef](index.md)<[VariablesType](index.md), [ResultType](index.md)>(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) : [BaseRef](../-base-ref/index.md)<[VariablesType](index.md), [ResultType](index.md)> + +## Constructors + +| | | +|---|---| +| [QueryRef](-query-ref.md) | [androidJvm]
constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) | + +## Properties + +| Name | Summary | +|---|---| +| [dataConnect](../-base-ref/data-connect.md) | [androidJvm]
val [dataConnect](../-base-ref/data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) | + +## Functions + +| Name | Summary | +|---|---| +| [execute](execute.md) | [androidJvm]
open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) | +| [execute](../../com.google.firebase.dataconnect.generated/execute.md) | [androidJvm]
suspend fun [QueryRef](index.md)<[GetPostQuery.Variables](../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md), [GetPostQuery.Result](../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md)>.[execute](../../com.google.firebase.dataconnect.generated/execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md) | +| [subscribe](subscribe.md) | [androidJvm]
fun [subscribe](subscribe.md)(variables: [VariablesType](index.md)): [QuerySubscription](../-query-subscription/index.md)<[VariablesType](index.md), [ResultType](index.md)> | +| [subscribe](../../com.google.firebase.dataconnect.generated/subscribe.md) | [androidJvm]
fun [QueryRef](index.md)<[GetPostQuery.Variables](../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md), [GetPostQuery.Result](../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md)>.[subscribe](../../com.google.firebase.dataconnect.generated/subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](../-query-subscription/index.md)<[GetPostQuery.Variables](../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md), [GetPostQuery.Result](../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/subscribe.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/subscribe.md new file mode 100644 index 00000000000..7d5f3e9b0a4 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/subscribe.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QueryRef](index.md)/[subscribe](subscribe.md) + +# subscribe + +[androidJvm]\ +fun [subscribe](subscribe.md)(variables: [VariablesType](index.md)): [QuerySubscription](../-query-subscription/index.md)<[VariablesType](index.md), [ResultType](index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/flow.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/flow.md new file mode 100644 index 00000000000..fa96b864074 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/flow.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md)/[flow](flow.md) + +# flow + +[androidJvm]\ +val [flow](flow.md): Flow<[Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/index.md new file mode 100644 index 00000000000..27cb6cabf6a --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/index.md @@ -0,0 +1,22 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md) + +# QuerySubscription + +[androidJvm]\ +class [QuerySubscription](index.md)<[VariablesType](index.md), [ResultType](index.md)> + +## Properties + +| Name | Summary | +|---|---| +| [flow](flow.md) | [androidJvm]
val [flow](flow.md): Flow<[Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>> | +| [lastResult](last-result.md) | [androidJvm]
val [lastResult](last-result.md): [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>? | +| [variables](variables.md) | [androidJvm]
val [variables](variables.md): [VariablesType](index.md) | + +## Functions + +| Name | Summary | +|---|---| +| [reload](reload.md) | [androidJvm]
fun [reload](reload.md)() | +| [update](update.md) | [androidJvm]
fun [update](update.md)(variables: [VariablesType](index.md)) | +| [update](../../com.google.firebase.dataconnect.generated/update.md) | [androidJvm]
fun [QuerySubscription](index.md)<[GetPostQuery.Variables](../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md), [GetPostQuery.Result](../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md)>.[update](../../com.google.firebase.dataconnect.generated/update.md)(block: [GetPostQuery.Variables.Builder](../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/last-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/last-result.md new file mode 100644 index 00000000000..27d2d5cbb41 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/last-result.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md)/[lastResult](last-result.md) + +# lastResult + +[androidJvm]\ +val [lastResult](last-result.md): [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>? diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/reload.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/reload.md new file mode 100644 index 00000000000..3af7dd6c9fb --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/reload.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md)/[reload](reload.md) + +# reload + +[androidJvm]\ +fun [reload](reload.md)() diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/update.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/update.md new file mode 100644 index 00000000000..f6c480fb962 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/update.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md)/[update](update.md) + +# update + +[androidJvm]\ +fun [update](update.md)(variables: [VariablesType](index.md)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/variables.md new file mode 100644 index 00000000000..8265530abee --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/variables.md @@ -0,0 +1,6 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md)/[variables](variables.md) + +# variables + +[androidJvm]\ +val [variables](variables.md): [VariablesType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-result-decode-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-result-decode-exception/index.md new file mode 100644 index 00000000000..06daec38556 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-result-decode-exception/index.md @@ -0,0 +1,26 @@ +//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[ResultDecodeException](index.md) + +# ResultDecodeException + +[androidJvm]\ +open class [ResultDecodeException](index.md) : [DataConnectException](../-data-connect-exception/index.md) + +## Properties + +| Name | Summary | +|---|---| +| [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | +| [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | + +## Functions + +| Name | Summary | +|---|---| +| [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | +| [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | +| [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | +| [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | +| [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | +| [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | +| [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/index.md new file mode 100644 index 00000000000..b7db742d870 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/index.md @@ -0,0 +1,25 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect](index.md) + +# Package-level declarations + +## Types + +| Name | Summary | +|---|---| +| [BaseRef](-base-ref/index.md) | [androidJvm]
abstract class [BaseRef](-base-ref/index.md)<[VariablesType](-base-ref/index.md), [ResultType](-base-ref/index.md)> | +| [DataConnectException](-data-connect-exception/index.md) | [androidJvm]
open class [DataConnectException](-data-connect-exception/index.md) : [Exception](https://developer.android.com/reference/kotlin/java/lang/Exception.html) | +| [FirebaseDataConnect](-firebase-data-connect/index.md) | [androidJvm]
class [FirebaseDataConnect](-firebase-data-connect/index.md) : [Closeable](https://developer.android.com/reference/kotlin/java/io/Closeable.html) | +| [FirebaseDataConnectSettings](-firebase-data-connect-settings/index.md) | [androidJvm]
class [FirebaseDataConnectSettings](-firebase-data-connect-settings/index.md) | +| [GraphQLException](-graph-q-l-exception/index.md) | [androidJvm]
open class [GraphQLException](-graph-q-l-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | +| [LogLevel](-log-level/index.md) | [androidJvm]
enum [LogLevel](-log-level/index.md) : [Enum](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-enum/index.html)<[LogLevel](-log-level/index.md)> | +| [MutationRef](-mutation-ref/index.md) | [androidJvm]
class [MutationRef](-mutation-ref/index.md)<[VariablesType](-mutation-ref/index.md), [ResultType](-mutation-ref/index.md)>(dataConnect: [FirebaseDataConnect](-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](-base-ref/-codec/index.md)<[VariablesType](-mutation-ref/index.md), [ResultType](-mutation-ref/index.md)>) : [BaseRef](-base-ref/index.md)<[VariablesType](-mutation-ref/index.md), [ResultType](-mutation-ref/index.md)> | +| [NetworkTransportException](-network-transport-exception/index.md) | [androidJvm]
open class [NetworkTransportException](-network-transport-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | +| [QueryRef](-query-ref/index.md) | [androidJvm]
class [QueryRef](-query-ref/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)>(dataConnect: [FirebaseDataConnect](-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](-base-ref/-codec/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)>) : [BaseRef](-base-ref/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)> | +| [QuerySubscription](-query-subscription/index.md) | [androidJvm]
class [QuerySubscription](-query-subscription/index.md)<[VariablesType](-query-subscription/index.md), [ResultType](-query-subscription/index.md)> | +| [ResultDecodeException](-result-decode-exception/index.md) | [androidJvm]
open class [ResultDecodeException](-result-decode-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | + +## Properties + +| Name | Summary | +|---|---| +| [logLevel](log-level.md) | [androidJvm]
@[Volatile](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-volatile/index.html)
var [logLevel](log-level.md): [LogLevel](-log-level/index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/log-level.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/log-level.md new file mode 100644 index 00000000000..2eb3bd38cab --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/log-level.md @@ -0,0 +1,9 @@ +//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect](index.md)/[logLevel](log-level.md) + +# logLevel + +[androidJvm]\ + +@[Volatile](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-volatile/index.html) + +var [logLevel](log-level.md): [LogLevel](-log-level/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/package-list b/firebase-dataconnect/ktdoc/firebase-dataconnect/package-list new file mode 100644 index 00000000000..2c14b989fa0 --- /dev/null +++ b/firebase-dataconnect/ktdoc/firebase-dataconnect/package-list @@ -0,0 +1,162 @@ +$dokka.format:gfm-v1 +$dokka.linkExtension:md +$dokka.location:com.google.firebase.dataconnect.apiproposal////PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal//execute/com.google.firebase.dataconnect.apiproposal.QueryRef[com.google.firebase.dataconnect.apiproposal.GetPostQuery.Variables,com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result]#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/execute.md +$dokka.location:com.google.firebase.dataconnect.apiproposal//getPost/com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect.Queries#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/get-post.md +$dokka.location:com.google.firebase.dataconnect.apiproposal//subscribe/com.google.firebase.dataconnect.apiproposal.QueryRef[com.google.firebase.dataconnect.apiproposal.GetPostQuery.Variables,com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result]#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/subscribe.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef.Codec///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef.Codec/decodeResult/#kotlin.collections.Map[kotlin.String,kotlin.Any?]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/decode-result.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef.Codec/encodeVariables/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/encode-variables.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef/dataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/data-connect.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef/execute/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/execute.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/DataConnectException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-data-connect-exception/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/ExecutionException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-execution-exception/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/FirebaseDataConnect.Queries///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/FirebaseDataConnect.Queries/dataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/data-connect.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/FirebaseDataConnect///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/FirebaseDataConnect/FirebaseDataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-firebase-data-connect.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/FirebaseDataConnect/queries/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/queries.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Companion///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Companion/query/#com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/query.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post.Comment///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post.Comment/Comment/#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/-comment.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post.Comment/content/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/content.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post.Comment/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/id.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post/Post/#kotlin.String#kotlin.collections.List[com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post.Comment]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-post.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post/comments/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/comments.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post/content/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/content.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result/Result/#com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-result.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result/post/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/post.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Variables///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Variables/Variables/#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/-variables.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Variables/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/id.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQueryRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-ref/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuerySubscription///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-subscription/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/NetworkTransportException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-network-transport-exception/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/QueryRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/QueryRef/QueryRef/#com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect#kotlin.String#kotlin.String#kotlin.String#com.google.firebase.dataconnect.apiproposal.BaseRef.Codec[TypeParam(bounds=[kotlin.Any?]),TypeParam(bounds=[kotlin.Any?])]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/-query-ref.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/QueryRef/execute/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/execute.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/QueryRef/subscribe/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/subscribe.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/index.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription/flow/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/flow.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription/lastResult/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/last-result.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription/query/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/query.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription/reload/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/reload.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription/variables/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/variables.md +$dokka.location:com.google.firebase.dataconnect.apiproposal/ResultDecodeException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md +$dokka.location:com.google.firebase.dataconnect.generated////PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md +$dokka.location:com.google.firebase.dataconnect.generated//createPost/com.google.firebase.dataconnect.FirebaseDataConnect.Mutations#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/create-post.md +$dokka.location:com.google.firebase.dataconnect.generated//execute/com.google.firebase.dataconnect.MutationRef[com.google.firebase.dataconnect.generated.CreatePostMutation.Variables,kotlin.Unit]#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md +$dokka.location:com.google.firebase.dataconnect.generated//execute/com.google.firebase.dataconnect.QueryRef[com.google.firebase.dataconnect.generated.GetPostQuery.Variables,com.google.firebase.dataconnect.generated.GetPostQuery.Result]#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md +$dokka.location:com.google.firebase.dataconnect.generated//getPost/com.google.firebase.dataconnect.FirebaseDataConnect.Queries#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/get-post.md +$dokka.location:com.google.firebase.dataconnect.generated//subscribe/com.google.firebase.dataconnect.QueryRef[com.google.firebase.dataconnect.generated.GetPostQuery.Variables,com.google.firebase.dataconnect.generated.GetPostQuery.Result]#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/subscribe.md +$dokka.location:com.google.firebase.dataconnect.generated//update/com.google.firebase.dataconnect.QuerySubscription[com.google.firebase.dataconnect.generated.GetPostQuery.Variables,com.google.firebase.dataconnect.generated.GetPostQuery.Result]#kotlin.Function1[com.google.firebase.dataconnect.generated.GetPostQuery.Variables.Builder,kotlin.Unit]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/update.md +$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Companion///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/index.md +$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Companion/mutation/#com.google.firebase.dataconnect.FirebaseDataConnect/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/mutation.md +$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables.PostData///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/index.md +$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables.PostData/PostData/#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/-post-data.md +$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables.PostData/content/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/content.md +$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables.PostData/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/id.md +$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md +$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables/Variables/#com.google.firebase.dataconnect.generated.CreatePostMutation.Variables.PostData/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-variables.md +$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables/data/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/data.md +$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/index.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Companion///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/index.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Companion/query/#com.google.firebase.dataconnect.FirebaseDataConnect/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/query.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post.Comment///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/index.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post.Comment/Comment/#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/-comment.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post.Comment/content/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/content.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post.Comment/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/id.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/index.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post/Post/#kotlin.String#kotlin.collections.List[com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post.Comment]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-post.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post/comments/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/comments.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post/content/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/content.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result/Result/#com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-result.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result/post/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/post.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables.Builder///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables.Builder/Builder/#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/-builder.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables.Builder/build/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/build.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables.Builder/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/id.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables/Variables/#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-variables.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables/build/#kotlin.Function1[com.google.firebase.dataconnect.generated.GetPostQuery.Variables.Builder,kotlin.Unit]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/build.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables/builder/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/builder.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/id.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/index.md +$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuerySubscription///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query-subscription/index.md +$dokka.location:com.google.firebase.dataconnect////PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/index.md +$dokka.location:com.google.firebase.dataconnect//logLevel/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/log-level.md +$dokka.location:com.google.firebase.dataconnect/BaseRef.Codec///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/index.md +$dokka.location:com.google.firebase.dataconnect/BaseRef.Codec/decodeResult/#kotlin.collections.Map[kotlin.String,kotlin.Any?]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/decode-result.md +$dokka.location:com.google.firebase.dataconnect/BaseRef.Codec/encodeVariables/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/encode-variables.md +$dokka.location:com.google.firebase.dataconnect/BaseRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/index.md +$dokka.location:com.google.firebase.dataconnect/BaseRef/dataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/data-connect.md +$dokka.location:com.google.firebase.dataconnect/BaseRef/execute/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/execute.md +$dokka.location:com.google.firebase.dataconnect/DataConnectException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-data-connect-exception/index.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Companion///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/index.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Companion/getInstance/#com.google.firebase.FirebaseApp#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Companion/getInstance/#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Mutations///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Mutations/dataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/data-connect.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Queries///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Queries/dataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/data-connect.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/index.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/app/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/app.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/close/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/close.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/location/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/location.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/mutations/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/mutations.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/queries/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/queries.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/service/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/service.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/settings/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/settings.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/toString/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/to-string.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/updateSettings/#kotlin.Function1[com.google.firebase.dataconnect.FirebaseDataConnectSettings.Builder,kotlin.Unit]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/update-settings.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/index.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/build/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/build/#kotlin.Function1[com.google.firebase.dataconnect.FirebaseDataConnectSettings.Builder,kotlin.Unit]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/connectToEmulator/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/connect-to-emulator.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/hostName/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/host-name.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/port/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/port.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/sslEnabled/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/ssl-enabled.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Companion///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/index.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Companion/defaults/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/defaults.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/index.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/builder/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/builder.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/equals/#kotlin.Any?/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/equals.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/hashCode/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/hash-code.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/hostName/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/host-name.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/port/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/port.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/sslEnabled/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/ssl-enabled.md +$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/toString/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/to-string.md +$dokka.location:com.google.firebase.dataconnect/GraphQLException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/index.md +$dokka.location:com.google.firebase.dataconnect/GraphQLException/errors/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/errors.md +$dokka.location:com.google.firebase.dataconnect/LogLevel.DEBUG///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-d-e-b-u-g/index.md +$dokka.location:com.google.firebase.dataconnect/LogLevel.INFO///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-i-n-f-o/index.md +$dokka.location:com.google.firebase.dataconnect/LogLevel.WARNING///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-w-a-r-n-i-n-g/index.md +$dokka.location:com.google.firebase.dataconnect/LogLevel///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/index.md +$dokka.location:com.google.firebase.dataconnect/LogLevel/entries/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/entries.md +$dokka.location:com.google.firebase.dataconnect/LogLevel/valueOf/#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/value-of.md +$dokka.location:com.google.firebase.dataconnect/LogLevel/values/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/values.md +$dokka.location:com.google.firebase.dataconnect/MutationRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/index.md +$dokka.location:com.google.firebase.dataconnect/MutationRef/MutationRef/#com.google.firebase.dataconnect.FirebaseDataConnect#kotlin.String#kotlin.String#kotlin.String#com.google.firebase.dataconnect.BaseRef.Codec[TypeParam(bounds=[kotlin.Any?]),TypeParam(bounds=[kotlin.Any?])]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/-mutation-ref.md +$dokka.location:com.google.firebase.dataconnect/MutationRef/execute/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/execute.md +$dokka.location:com.google.firebase.dataconnect/NetworkTransportException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-network-transport-exception/index.md +$dokka.location:com.google.firebase.dataconnect/QueryRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/index.md +$dokka.location:com.google.firebase.dataconnect/QueryRef/QueryRef/#com.google.firebase.dataconnect.FirebaseDataConnect#kotlin.String#kotlin.String#kotlin.String#com.google.firebase.dataconnect.BaseRef.Codec[TypeParam(bounds=[kotlin.Any?]),TypeParam(bounds=[kotlin.Any?])]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/-query-ref.md +$dokka.location:com.google.firebase.dataconnect/QueryRef/execute/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/execute.md +$dokka.location:com.google.firebase.dataconnect/QueryRef/subscribe/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/subscribe.md +$dokka.location:com.google.firebase.dataconnect/QuerySubscription///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/index.md +$dokka.location:com.google.firebase.dataconnect/QuerySubscription/flow/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/flow.md +$dokka.location:com.google.firebase.dataconnect/QuerySubscription/lastResult/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/last-result.md +$dokka.location:com.google.firebase.dataconnect/QuerySubscription/reload/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/reload.md +$dokka.location:com.google.firebase.dataconnect/QuerySubscription/update/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/update.md +$dokka.location:com.google.firebase.dataconnect/QuerySubscription/variables/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/variables.md +$dokka.location:com.google.firebase.dataconnect/ResultDecodeException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-result-decode-exception/index.md +com.google.firebase.dataconnect +com.google.firebase.dataconnect.apiproposal +com.google.firebase.dataconnect.generated + diff --git a/firebase-dataconnect/ktdoc/index.md b/firebase-dataconnect/ktdoc/index.md new file mode 100644 index 00000000000..fd7e2dbff73 --- /dev/null +++ b/firebase-dataconnect/ktdoc/index.md @@ -0,0 +1,11 @@ +//[firebase-dataconnect](index.md) + +# firebase-dataconnect + +## Packages + +| Name | +|---| +| [com.google.firebase.dataconnect](firebase-dataconnect/com.google.firebase.dataconnect/index.md) | +| [com.google.firebase.dataconnect.apiproposal](firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md) | +| [com.google.firebase.dataconnect.generated](firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md) | From 345816e3a8032760d8876966fcba72321e73e952 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 7 Nov 2023 14:36:11 -0500 Subject: [PATCH 045/573] QueryApiProposal.kt: update exceptions --- .../dataconnect/apiproposal/QueryApiProposal.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index 186606e0af1..444ba3eafa2 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -18,16 +18,16 @@ class FirebaseDataConnect { val queries: Queries = TODO() } -open class DataConnectException internal constructor(message: String, cause: Throwable? = null) : - Exception(message, cause) +open class DataConnectException internal constructor() : Exception() -open class NetworkTransportException internal constructor(message: String, cause: Throwable) : - DataConnectException(message, cause) +open class NetworkTransportException internal constructor() : DataConnectException() -open class ExecutionException internal constructor(message: String) : DataConnectException(message) +open class GraphQLException internal constructor() : DataConnectException() { + val errors: List + get() = TODO() +} -open class ResultDecodeException internal constructor(message: String) : - DataConnectException(message) +open class ResultDecodeException internal constructor() : DataConnectException() abstract class BaseRef internal constructor() { val dataConnect: FirebaseDataConnect From b5dd471a30479b55535ec4ad6fe49f1c4b1516c5 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 7 Nov 2023 16:11:37 -0500 Subject: [PATCH 046/573] make result nullable --- .../dataconnect/generated/PostsTest.kt | 17 +++++++++--- .../apiproposal/QueryApiProposal.kt | 19 +++++++------- .../dataconnect/generated/GetPostQuery.kt | 26 +++++++++++-------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt index 560befacddd..ceac984a420 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt @@ -40,6 +40,15 @@ class PostsTest { @JvmField @Rule val dataConnectFactory = TestDataConnectFactory() @JvmField @Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @Test + fun getPostWithNonExistingId() { + val dc = dataConnectFactory.newInstance(service = "local") + runBlocking { + val queryResponse = dc.queries.getPost.execute(id = UUID.randomUUID().toString()) + assertWithMessage("queryResponse").that(queryResponse).isNull() + } + } + @Test fun createPostThenGetPost() { val dc = dataConnectFactory.newInstance(service = "local") @@ -52,7 +61,7 @@ class PostsTest { val queryResponse = dc.queries.getPost.execute(id = postId) assertWithMessage("queryResponse") - .that(queryResponse.post) + .that(queryResponse?.post) .isEqualTo(GetPostQuery.Result.Post(content = postContent, comments = emptyList())) } } @@ -76,7 +85,7 @@ class PostsTest { val result1 = querySubscription.flow.timeout(5.seconds).first() assertWithMessage("result1.isSuccess").that(result1.isSuccess).isTrue() assertWithMessage("result1.post.content") - .that(result1.getOrThrow().post.content) + .that(result1.getOrThrow()?.post?.content) .isEqualTo(postContent1) assertWithMessage("lastResult 1").that(querySubscription.lastResult).isEqualTo(result1) @@ -90,10 +99,10 @@ class PostsTest { assertWithMessage("results2[0].isSuccess").that(results2[0].isSuccess).isTrue() assertWithMessage("results2[1].isSuccess").that(results2[1].isSuccess).isTrue() assertWithMessage("results2[0].post.content") - .that(results2[0].getOrThrow().post.content) + .that(results2[0].getOrThrow()?.post?.content) .isEqualTo(postContent1) assertWithMessage("results2[1].post.content") - .that(results2[1].getOrThrow().post.content) + .that(results2[1].getOrThrow()?.post?.content) .isEqualTo(postContent2) assertWithMessage("lastResult 2").that(querySubscription.lastResult).isEqualTo(results2[1]) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index 444ba3eafa2..a168d08e46a 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -91,24 +91,24 @@ class GetPostQuery private constructor() { } companion object { - fun query(dataConnect: FirebaseDataConnect): QueryRef = TODO() + fun query(dataConnect: FirebaseDataConnect): QueryRef = TODO() } } -val FirebaseDataConnect.Queries.getPost: QueryRef +val FirebaseDataConnect.Queries.getPost: QueryRef get() = TODO() -suspend fun QueryRef.execute( +suspend fun QueryRef.execute( id: String -): GetPostQuery.Result = TODO() +): GetPostQuery.Result? = TODO() -fun QueryRef.subscribe( +fun QueryRef.subscribe( id: String -): QuerySubscription = TODO() +): QuerySubscription = TODO() -typealias GetPostQueryRef = QueryRef +typealias GetPostQueryRef = QueryRef -typealias GetPostQuerySubscription = QuerySubscription +typealias GetPostQuerySubscription = QuerySubscription //////////////////////////////////////////////////////////////////////////////////////////////////// // CUSTOMER CODE @@ -118,8 +118,7 @@ private class MainActivity : Activity() { private lateinit var dataConnect: FirebaseDataConnect private lateinit var activityCoroutineScope: CoroutineScope - private var querySubscription: QuerySubscription? = - null + private var querySubscription: GetPostQuerySubscription? = null private var querySubscriptionFlow: Job? = null fun onLiveUpdateButtonClick() { diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt index b102375ebaf..188e502144f 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -47,18 +47,22 @@ class GetPostQuery private constructor() { ) private val codec = - object : BaseRef.Codec { + object : BaseRef.Codec { override fun encodeVariables(variables: Variables) = mapOf("id" to variables.id) override fun decodeResult(map: Map) = - Result( - map["post"].let { - _decodePost( - data = it as? Map<*, *> ?: decodeError("post", it, "Map", "result", map), - path = "post" + map["post"].let { + if (it == null) { + null + } else { + Result( + _decodePost( + data = it as? Map<*, *> ?: decodeError("post", it, "Map", "result", map), + path = "post" + ) ) } - ) + } private fun _decodePost(data: Map<*, *>, path: String) = Result.Post( @@ -108,18 +112,18 @@ class GetPostQuery private constructor() { } } -typealias GetPostQuerySubscription = QuerySubscription +typealias GetPostQuerySubscription = QuerySubscription val FirebaseDataConnect.Queries.getPost get() = GetPostQuery.query(dataConnect) -suspend fun QueryRef.execute(id: String) = +suspend fun QueryRef.execute(id: String) = execute(variablesFor(id = id)) -fun QueryRef.subscribe(id: String) = +fun QueryRef.subscribe(id: String) = subscribe(variablesFor(id = id)) -fun QuerySubscription.update( +fun QuerySubscription.update( block: GetPostQuery.Variables.Builder.() -> Unit ) = update(variables.build(block)) From 930e32fac3edec6d3a9a75838ddb1bcaa2860715 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 7 Nov 2023 16:51:12 -0500 Subject: [PATCH 047/573] QuerySubscription.Message added --- .../dataconnect/QuerySubscriptionTest.kt | 16 ++++---- .../dataconnect/generated/PostsTest.kt | 12 +++--- .../firebase/dataconnect/QuerySubscription.kt | 40 +++++++++++++------ .../apiproposal/QueryApiProposal.kt | 27 ++++++++----- 4 files changed, 61 insertions(+), 34 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt index 4dc0f61520a..0cf60b8bf95 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt @@ -74,7 +74,7 @@ class QuerySubscriptionTest { val resultsChannel = Channel(capacity = Channel.UNLIMITED) val collectJob = launch { - querySubscription.flow.collect { resultsChannel.send(it.getOrThrow()) } + querySubscription.flow.collect { resultsChannel.send(it.result.getOrThrow()) } } val result1 = resultsChannel.receive() @@ -96,12 +96,12 @@ class QuerySubscriptionTest { val querySubscription = schema.getPerson.subscribe(id = "TestId12345") withTimeout(2.seconds) { - val result1 = querySubscription.flow.first().getOrThrow() + val result1 = querySubscription.flow.first().result.getOrThrow() assertThat(result1).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) schema.updatePerson.execute(id = "TestId12345", name = "TestName2", age = 10002) - val result2 = querySubscription.flow.first().getOrThrow() + val result2 = querySubscription.flow.first().result.getOrThrow() assertThat(result2).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) } } @@ -118,7 +118,7 @@ class QuerySubscriptionTest { repeat(5) { assertWithMessage("fast flow retrieval iteration $it") - .that(querySubscription.flow.first().getOrThrow()) + .that(querySubscription.flow.first().result.getOrThrow()) .isEqualToGetPersonQueryResult(name = "TestName", age = 10000) } @@ -135,12 +135,12 @@ class QuerySubscriptionTest { val resultsChannel1 = Channel(capacity = Channel.UNLIMITED) val flowJob1 = launch { - querySubscription.flow.collect { resultsChannel1.send(it.getOrThrow()) } + querySubscription.flow.collect { resultsChannel1.send(it.result.getOrThrow()) } } val resultsChannel2 = Channel(capacity = Channel.UNLIMITED) val flowJob2 = launch { - querySubscription.flow.collect { resultsChannel2.send(it.getOrThrow()) } + querySubscription.flow.collect { resultsChannel2.send(it.result.getOrThrow()) } } resultsChannel1.purge(0.25.seconds).forEach { assertThat(it).isEqualToGetPersonQueryResult(name = "TestName0", age = 10000) @@ -170,7 +170,9 @@ class QuerySubscriptionTest { withTimeout(5.seconds) { val resultsChannel = Channel>(capacity = Channel.UNLIMITED) - val collectJob = launch { querySubscription.flow.collect(resultsChannel::send) } + val collectJob = launch { + querySubscription.flow.map { it.result }.collect(resultsChannel::send) + } val maxHardwareConcurrency = Math.max(2, Runtime.getRuntime().availableProcessors()) val multiThreadExecutor = Executors.newFixedThreadPool(maxHardwareConcurrency) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt index ceac984a420..b520147c532 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt @@ -83,9 +83,9 @@ class PostsTest { assertWithMessage("lastResult 0").that(querySubscription.lastResult).isNull() val result1 = querySubscription.flow.timeout(5.seconds).first() - assertWithMessage("result1.isSuccess").that(result1.isSuccess).isTrue() + assertWithMessage("result1.isSuccess").that(result1.result.isSuccess).isTrue() assertWithMessage("result1.post.content") - .that(result1.getOrThrow()?.post?.content) + .that(result1.result.getOrThrow()?.post?.content) .isEqualTo(postContent1) assertWithMessage("lastResult 1").that(querySubscription.lastResult).isEqualTo(result1) @@ -96,13 +96,13 @@ class PostsTest { val results2 = flow2Job.await() assertWithMessage("results2.size").that(results2.size).isEqualTo(2) - assertWithMessage("results2[0].isSuccess").that(results2[0].isSuccess).isTrue() - assertWithMessage("results2[1].isSuccess").that(results2[1].isSuccess).isTrue() + assertWithMessage("results2[0].isSuccess").that(results2[0].result.isSuccess).isTrue() + assertWithMessage("results2[1].isSuccess").that(results2[1].result.isSuccess).isTrue() assertWithMessage("results2[0].post.content") - .that(results2[0].getOrThrow()?.post?.content) + .that(results2[0].result.getOrThrow()?.post?.content) .isEqualTo(postContent1) assertWithMessage("results2[1].post.content") - .that(results2[1].getOrThrow()?.post?.content) + .that(results2[1].result.getOrThrow()?.post?.content) .isEqualTo(postContent2) assertWithMessage("lastResult 2").that(querySubscription.lastResult).isEqualTo(results2[1]) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt index b8a09fc273a..36c6e301403 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt @@ -29,7 +29,10 @@ internal constructor( get() = _variables.get() private val sharedFlow = - MutableSharedFlow>(replay = 1, extraBufferCapacity = Integer.MAX_VALUE) + MutableSharedFlow>( + replay = 1, + extraBufferCapacity = Integer.MAX_VALUE + ) private val sequentialDispatcher = FirebaseExecutors.newSequentialExecutor(query.dataConnect.nonBlockingExecutor) .asCoroutineDispatcher() @@ -40,7 +43,7 @@ internal constructor( private var reloadInProgress = false private var pendingReload = false - val lastResult + val lastResult: Message? get() = sharedFlow.replayCache.firstOrNull() fun reload() { @@ -49,7 +52,7 @@ internal constructor( if (!reloadInProgress) { reloadInProgress = true try { - doReload() + doReloadLoop() } finally { reloadInProgress = false } @@ -62,19 +65,32 @@ internal constructor( reload() } - val flow: Flow> + val flow: Flow> get() = sharedFlow.asSharedFlow().onSubscription { reload() }.buffer(Channel.CONFLATED) - private suspend fun doReload() { + private suspend fun doReloadLoop() { while (pendingReload) { pendingReload = false - val result = - try { - Result.success(query.execute(variables)) - } catch (e: Throwable) { - Result.failure(e) - } - sharedFlow.emit(result) + sharedFlow.emit(reload(variables, query)) } } + + class Message( + val variables: VariablesType, + val result: Result + ) } + +private suspend fun reload( + variables: VariablesType, + query: QueryRef +) = + QuerySubscription.Message( + variables = variables, + result = + try { + Result.success(query.execute(variables)) + } catch (e: Throwable) { + Result.failure(e) + } + ) diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index a168d08e46a..db265245092 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -62,7 +62,7 @@ class QuerySubscription internal constructor() { // Alternative considered: add `lastResult`. The problem is, what do we do with this value if the // variables are changed via a call to update()? Do we clear it? Or do we leave it there even // though it came from a request with potentially-different variables? - val lastResult: Result? + val lastResult: Message? get() = TODO() // Alternative considered: Return `Deferred>` so that customer knows when the reload @@ -73,7 +73,12 @@ class QuerySubscription internal constructor() { // some previous call to reload() by some other unrelated operation. fun reload(): Unit = TODO() - val flow: Flow> = TODO() + val flow: Flow> = TODO() + + class Message( + val variables: VariablesType, + val result: Result + ) } //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -128,10 +133,10 @@ private class MainActivity : Activity() { querySubscriptionFlow = activityCoroutineScope.launch { it.flow.collect { - if (it.isFailure) { - showError(it.exceptionOrNull().toString()) + if (it.result.isSuccess) { + showPostContent(it.variables.id, it.result.getOrThrow()) } else { - showPostContent(it.getOrThrow().post.content) + showError(it.variables.id, it.result.exceptionOrNull()!!) } } } @@ -145,8 +150,12 @@ private class MainActivity : Activity() { fun onLoadButtonClick() { activityCoroutineScope.launch { - val result = dataConnect.queries.getPost.execute(id = getIdFromTextView()) - showPostContent(result.post.content) + val id = getIdFromTextView() + try { + showPostContent(id, dataConnect.queries.getPost.execute(id = id)) + } catch (e: Exception) { + showError(id, e) + } } } @@ -156,6 +165,6 @@ private class MainActivity : Activity() { } fun getIdFromTextView(): String = TODO() - fun showError(errorMessage: String): Unit = TODO() - fun showPostContent(content: String): Unit = TODO() + fun showError(postId: String, exception: Throwable): Unit = TODO() + fun showPostContent(postId: String, post: GetPostQuery.Result?): Unit = TODO() } From cef9d2d0b4ffb739c9cb69814912cefab328edec Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Nov 2023 12:44:24 -0500 Subject: [PATCH 048/573] Make QueryRef and MutationRef constructors internal, and add query() and mutation() factory methods to FirebaseDataConnect --- .../dataconnect/FirebaseDataConnectTest.kt | 23 ++++++--------- .../testutil/schemas/PersonSchema.kt | 12 +++----- .../dataconnect/FirebaseDataConnect.kt | 28 +++++++++++++++++++ .../firebase/dataconnect/MutationRef.kt | 3 +- .../google/firebase/dataconnect/QueryRef.kt | 3 +- .../apiproposal/QueryApiProposal.kt | 17 ++++++----- .../generated/CreatePostMutation.kt | 3 +- .../dataconnect/generated/GetPostQuery.kt | 3 +- 8 files changed, 57 insertions(+), 35 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 91d02276057..69b549a6635 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -203,8 +203,7 @@ class FirebaseDataConnectTest { run { val mutation = - MutationRef( - dataConnect = dc, + dc.mutation( operationName = "createPost", operationSet = "crud", revision = "TestRevision", @@ -219,12 +218,11 @@ class FirebaseDataConnectTest { run { val query = - QueryRef( - dataConnect = dc, + dc.query( operationName = "getPost", operationSet = "crud", revision = "TestRevision", - codec = IdentityCodec, + codec = IdentityCodec ) val queryResult = query.execute(mapOf("id" to postId)) assertWithMessage("queryResponse") @@ -238,12 +236,11 @@ class FirebaseDataConnectTest { @Test fun testInstallEmulatorSchema() { suspend fun FirebaseDataConnect.createPerson(id: String, name: String, age: Int? = null) = - MutationRef( - dataConnect = this, + mutation( operationName = "createPerson", operationSet = "ops", revision = "42", - codec = IdentityCodec, + codec = IdentityCodec ) .execute( mapOf( @@ -257,22 +254,20 @@ class FirebaseDataConnectTest { ) suspend fun FirebaseDataConnect.getPerson(id: String) = - QueryRef( - dataConnect = this, + query( operationName = "getPerson", operationSet = "ops", revision = "42", - codec = IdentityCodec, + codec = IdentityCodec ) .execute(mapOf("id" to id)) suspend fun FirebaseDataConnect.getAllPeople() = - QueryRef( - dataConnect = this, + query( operationName = "getAllPeople", operationSet = "ops", revision = "42", - codec = IdentityCodec, + codec = IdentityCodec ) .execute(emptyMap()) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index e3bade69da4..f1dcd9bd1a3 100644 --- a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -27,8 +27,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { } val createPerson = - MutationRef( - dataConnect = dataConnect, + dataConnect.mutation( operationName = "createPerson", operationSet = "ops", revision = "42", @@ -57,8 +56,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { } val updatePerson = - MutationRef( - dataConnect = dataConnect, + dataConnect.mutation( operationName = "updatePerson", operationSet = "ops", revision = "42", @@ -88,8 +86,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { } val deletePerson = - MutationRef( - dataConnect = dataConnect, + dataConnect.mutation( operationName = "deletePerson", operationSet = "ops", revision = "42", @@ -109,8 +106,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { } val getPerson = - QueryRef( - dataConnect = dataConnect, + dataConnect.query( operationName = "getPerson", operationSet = "ops", revision = "42", diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt index e9ef3af9a2f..669697580d8 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -126,6 +126,34 @@ internal constructor( ) ) + fun query( + operationName: String, + operationSet: String, + revision: String, + codec: BaseRef.Codec + ): QueryRef = + QueryRef( + dataConnect = this, + operationName = operationName, + operationSet = operationSet, + revision = revision, + codec = codec + ) + + fun mutation( + operationName: String, + operationSet: String, + revision: String, + codec: BaseRef.Codec + ): MutationRef = + MutationRef( + dataConnect = this, + operationName = operationName, + operationSet = operationSet, + revision = revision, + codec = codec + ) + override fun close() { logger.debug { "close() called" } lock.write { diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt index 87c9165c6b8..76ca038e891 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt @@ -13,7 +13,8 @@ // limitations under the License. package com.google.firebase.dataconnect -class MutationRef( +class MutationRef +internal constructor( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt index e58219ecdff..100e17ac563 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt @@ -13,7 +13,8 @@ // limitations under the License. package com.google.firebase.dataconnect -class QueryRef( +class QueryRef +internal constructor( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index db265245092..899b654b4c6 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -11,6 +11,14 @@ import kotlinx.coroutines.launch //////////////////////////////////////////////////////////////////////////////////////////////////// class FirebaseDataConnect { + + fun query( + operationName: String, + operationSet: String, + revision: String, + codec: BaseRef.Codec + ): QueryRef = TODO() + class Queries internal constructor() { val dataConnect: FirebaseDataConnect get() = TODO() @@ -41,13 +49,8 @@ abstract class BaseRef internal constructor() { } } -class QueryRef( - dataConnect: FirebaseDataConnect, - operationName: String, - operationSet: String, - revision: String, - codec: Codec, -) : BaseRef() { +class QueryRef internal constructor() : + BaseRef() { override suspend fun execute(variables: VariablesType): ResultType = TODO() fun subscribe(variables: VariablesType): QuerySubscription = TODO() diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt index 5d739ae6720..3ee43f67ae8 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt @@ -26,8 +26,7 @@ class CreatePostMutation private constructor() { companion object { fun mutation(dataConnect: FirebaseDataConnect) = - MutationRef( - dataConnect = dataConnect, + dataConnect.mutation( operationName = "createPost", operationSet = "crud", revision = "1234567890abcdef", diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt index 188e502144f..21294b376a4 100644 --- a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -38,8 +38,7 @@ class GetPostQuery private constructor() { companion object { fun query(dataConnect: FirebaseDataConnect) = - QueryRef( - dataConnect, + dataConnect.query( operationName = "getPost", operationSet = "crud", revision = "1234567890abcdef", From 1c157218ba7a15dabe1f4d693d377b7287dc93f4 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Nov 2023 15:06:11 -0500 Subject: [PATCH 049/573] rename java directories to kotlin --- .../com/google/firebase/dataconnect/FirebaseDataConnectTest.kt | 0 .../com/google/firebase/dataconnect/QuerySubscriptionTest.kt | 0 .../com/google/firebase/dataconnect/generated/PostsTest.kt | 0 .../firebase/dataconnect/testutil/DataConnectLogLevelRule.kt | 0 .../google/firebase/dataconnect/testutil/EmulatorController.kt | 0 .../com/google/firebase/dataconnect/testutil/FactoryTestRule.kt | 0 .../com/google/firebase/dataconnect/testutil/IdentityCodec.kt | 0 .../firebase/dataconnect/testutil/TestDataConnectFactory.kt | 0 .../firebase/dataconnect/testutil/TestFirebaseAppFactory.kt | 0 .../google/firebase/dataconnect/testutil/schemas/PersonSchema.kt | 0 .../{java => kotlin}/com/google/firebase/dataconnect/BaseRef.kt | 0 .../com/google/firebase/dataconnect/DataConnectGrpcClient.kt | 0 .../com/google/firebase/dataconnect/FirebaseDataConnect.kt | 0 .../com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt | 0 .../google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt | 0 .../google/firebase/dataconnect/FirebaseDataConnectSettings.kt | 0 .../{java => kotlin}/com/google/firebase/dataconnect/Logger.kt | 0 .../com/google/firebase/dataconnect/MutationRef.kt | 0 .../{java => kotlin}/com/google/firebase/dataconnect/QueryRef.kt | 0 .../com/google/firebase/dataconnect/QuerySubscription.kt | 0 .../google/firebase/dataconnect/apiproposal/QueryApiProposal.kt | 0 .../google/firebase/dataconnect/generated/CreatePostMutation.kt | 0 .../com/google/firebase/dataconnect/generated/GetPostQuery.kt | 0 .../firebase/dataconnect/FirebaseDataConnectSettingsTest.kt | 0 24 files changed, 0 insertions(+), 0 deletions(-) rename firebase-dataconnect/src/androidTest/{java => kotlin}/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt (100%) rename firebase-dataconnect/src/androidTest/{java => kotlin}/com/google/firebase/dataconnect/QuerySubscriptionTest.kt (100%) rename firebase-dataconnect/src/androidTest/{java => kotlin}/com/google/firebase/dataconnect/generated/PostsTest.kt (100%) rename firebase-dataconnect/src/androidTest/{java => kotlin}/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt (100%) rename firebase-dataconnect/src/androidTest/{java => kotlin}/com/google/firebase/dataconnect/testutil/EmulatorController.kt (100%) rename firebase-dataconnect/src/androidTest/{java => kotlin}/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt (100%) rename firebase-dataconnect/src/androidTest/{java => kotlin}/com/google/firebase/dataconnect/testutil/IdentityCodec.kt (100%) rename firebase-dataconnect/src/androidTest/{java => kotlin}/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt (100%) rename firebase-dataconnect/src/androidTest/{java => kotlin}/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt (100%) rename firebase-dataconnect/src/androidTest/{java => kotlin}/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/BaseRef.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/DataConnectGrpcClient.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/FirebaseDataConnect.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/Logger.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/MutationRef.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/QueryRef.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/QuerySubscription.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/generated/CreatePostMutation.kt (100%) rename firebase-dataconnect/src/main/{java => kotlin}/com/google/firebase/dataconnect/generated/GetPostQuery.kt (100%) rename firebase-dataconnect/src/test/{java => kotlin}/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt (100%) diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt similarity index 100% rename from firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt similarity index 100% rename from firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/QuerySubscriptionTest.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt similarity index 100% rename from firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/generated/PostsTest.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt similarity index 100% rename from firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/EmulatorController.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt similarity index 100% rename from firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/EmulatorController.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt similarity index 100% rename from firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/IdentityCodec.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/IdentityCodec.kt similarity index 100% rename from firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/IdentityCodec.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/IdentityCodec.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt similarity index 100% rename from firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt similarity index 100% rename from firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt diff --git a/firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt similarity index 100% rename from firebase-dataconnect/src/androidTest/java/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/BaseRef.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/DataConnectGrpcClient.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnect.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectRegistrar.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/Logger.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/MutationRef.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QueryRef.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/QuerySubscription.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/CreatePostMutation.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt diff --git a/firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt similarity index 100% rename from firebase-dataconnect/src/main/java/com/google/firebase/dataconnect/generated/GetPostQuery.kt rename to firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt diff --git a/firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt similarity index 100% rename from firebase-dataconnect/src/test/java/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt rename to firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt From 5360190b5976353df5f7e2e26e8647b5536545cf Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Nov 2023 15:07:34 -0500 Subject: [PATCH 050/573] removing accidentally-added 'ktdoc' directory --- .../-base-ref/-codec/decode-result.md | 6 - .../-base-ref/-codec/encode-variables.md | 6 - .../-base-ref/-codec/index.md | 13 -- .../-base-ref/data-connect.md | 6 - .../-base-ref/execute.md | 6 - .../-base-ref/index.md | 29 ---- .../-data-connect-exception/index.md | 33 ---- .../-execution-exception/index.md | 26 --- .../-firebase-data-connect.md | 6 - .../-queries/data-connect.md | 6 - .../-firebase-data-connect/-queries/index.md | 13 -- .../-firebase-data-connect/index.md | 24 --- .../-firebase-data-connect/queries.md | 6 - .../-get-post-query-ref/index.md | 6 - .../-get-post-query-subscription/index.md | 6 - .../-get-post-query/-companion/index.md | 12 -- .../-get-post-query/-companion/query.md | 6 - .../-result/-post/-comment/-comment.md | 6 - .../-result/-post/-comment/content.md | 6 - .../-result/-post/-comment/id.md | 6 - .../-result/-post/-comment/index.md | 19 -- .../-get-post-query/-result/-post/-post.md | 6 - .../-get-post-query/-result/-post/comments.md | 6 - .../-get-post-query/-result/-post/content.md | 6 - .../-get-post-query/-result/-post/index.md | 25 --- .../-get-post-query/-result/-result.md | 6 - .../-get-post-query/-result/index.md | 24 --- .../-get-post-query/-result/post.md | 6 - .../-get-post-query/-variables/-variables.md | 6 - .../-get-post-query/-variables/id.md | 6 - .../-get-post-query/-variables/index.md | 18 -- .../-get-post-query/index.md | 14 -- .../-network-transport-exception/index.md | 26 --- .../-query-ref/-query-ref.md | 6 - .../-query-ref/execute.md | 6 - .../-query-ref/index.md | 27 --- .../-query-ref/subscribe.md | 6 - .../-query-subscription/flow.md | 6 - .../-query-subscription/index.md | 21 --- .../-query-subscription/last-result.md | 6 - .../-query-subscription/query.md | 6 - .../-query-subscription/reload.md | 6 - .../-query-subscription/variables.md | 6 - .../-result-decode-exception/index.md | 26 --- .../execute.md | 6 - .../get-post.md | 6 - .../index.md | 32 ---- .../subscribe.md | 6 - .../-create-post-mutation/-companion/index.md | 12 -- .../-companion/mutation.md | 6 - .../-variables/-post-data/-post-data.md | 6 - .../-variables/-post-data/content.md | 6 - .../-variables/-post-data/id.md | 6 - .../-variables/-post-data/index.md | 19 -- .../-variables/-variables.md | 6 - .../-create-post-mutation/-variables/data.md | 6 - .../-create-post-mutation/-variables/index.md | 24 --- .../-create-post-mutation/index.md | 13 -- .../-get-post-query-subscription/index.md | 6 - .../-get-post-query/-companion/index.md | 12 -- .../-get-post-query/-companion/query.md | 6 - .../-result/-post/-comment/-comment.md | 6 - .../-result/-post/-comment/content.md | 6 - .../-result/-post/-comment/id.md | 6 - .../-result/-post/-comment/index.md | 19 -- .../-get-post-query/-result/-post/-post.md | 6 - .../-get-post-query/-result/-post/comments.md | 6 - .../-get-post-query/-result/-post/content.md | 6 - .../-get-post-query/-result/-post/index.md | 25 --- .../-get-post-query/-result/-result.md | 6 - .../-get-post-query/-result/index.md | 24 --- .../-get-post-query/-result/post.md | 6 - .../-variables/-builder/-builder.md | 6 - .../-variables/-builder/build.md | 6 - .../-get-post-query/-variables/-builder/id.md | 6 - .../-variables/-builder/index.md | 24 --- .../-get-post-query/-variables/-variables.md | 6 - .../-get-post-query/-variables/build.md | 6 - .../-get-post-query/-variables/builder.md | 6 - .../-get-post-query/-variables/id.md | 6 - .../-get-post-query/-variables/index.md | 31 ---- .../-get-post-query/index.md | 14 -- .../create-post.md | 6 - .../execute.md | 8 - .../get-post.md | 6 - .../index.md | 26 --- .../subscribe.md | 6 - .../update.md | 6 - .../-base-ref/-codec/decode-result.md | 6 - .../-base-ref/-codec/encode-variables.md | 6 - .../-base-ref/-codec/index.md | 13 -- .../-base-ref/data-connect.md | 6 - .../-base-ref/execute.md | 6 - .../-base-ref/index.md | 30 ---- .../-data-connect-exception/index.md | 33 ---- .../-builder/build.md | 8 - .../-builder/connect-to-emulator.md | 6 - .../-builder/host-name.md | 6 - .../-builder/index.md | 21 --- .../-builder/port.md | 6 - .../-builder/ssl-enabled.md | 6 - .../-companion/defaults.md | 6 - .../-companion/index.md | 12 -- .../builder.md | 6 - .../-firebase-data-connect-settings/equals.md | 6 - .../hash-code.md | 6 - .../host-name.md | 6 - .../-firebase-data-connect-settings/index.md | 30 ---- .../-firebase-data-connect-settings/port.md | 6 - .../ssl-enabled.md | 6 - .../to-string.md | 6 - .../-companion/get-instance.md | 8 - .../-companion/index.md | 12 -- .../-mutations/data-connect.md | 6 - .../-mutations/index.md | 13 -- .../-queries/data-connect.md | 6 - .../-firebase-data-connect/-queries/index.md | 13 -- .../-firebase-data-connect/app.md | 6 - .../-firebase-data-connect/close.md | 6 - .../-firebase-data-connect/index.md | 33 ---- .../-firebase-data-connect/location.md | 6 - .../-firebase-data-connect/mutations.md | 6 - .../-firebase-data-connect/queries.md | 6 - .../-firebase-data-connect/service.md | 6 - .../-firebase-data-connect/settings.md | 6 - .../-firebase-data-connect/to-string.md | 6 - .../-firebase-data-connect/update-settings.md | 6 - .../-graph-q-l-exception/errors.md | 6 - .../-graph-q-l-exception/index.md | 27 --- .../-log-level/-d-e-b-u-g/index.md | 13 -- .../-log-level/-i-n-f-o/index.md | 13 -- .../-log-level/-w-a-r-n-i-n-g/index.md | 13 -- .../-log-level/entries.md | 10 -- .../-log-level/index.md | 29 ---- .../-log-level/value-of.md | 14 -- .../-log-level/values.md | 10 -- .../-mutation-ref/-mutation-ref.md | 6 - .../-mutation-ref/execute.md | 6 - .../-mutation-ref/index.md | 25 --- .../-network-transport-exception/index.md | 26 --- .../-query-ref/-query-ref.md | 6 - .../-query-ref/execute.md | 6 - .../-query-ref/index.md | 27 --- .../-query-ref/subscribe.md | 6 - .../-query-subscription/flow.md | 6 - .../-query-subscription/index.md | 22 --- .../-query-subscription/last-result.md | 6 - .../-query-subscription/reload.md | 6 - .../-query-subscription/update.md | 6 - .../-query-subscription/variables.md | 6 - .../-result-decode-exception/index.md | 26 --- .../com.google.firebase.dataconnect/index.md | 25 --- .../log-level.md | 9 - .../ktdoc/firebase-dataconnect/package-list | 162 ------------------ firebase-dataconnect/ktdoc/index.md | 11 -- 155 files changed, 1897 deletions(-) delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/decode-result.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/encode-variables.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/data-connect.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/execute.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-data-connect-exception/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-execution-exception/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-firebase-data-connect.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/data-connect.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/queries.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-ref/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-subscription/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/query.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/-comment.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/content.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/id.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-post.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/comments.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/content.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-result.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/post.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/-variables.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/id.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-network-transport-exception/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/-query-ref.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/execute.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/subscribe.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/flow.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/last-result.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/query.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/reload.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/variables.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/execute.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/get-post.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/subscribe.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/mutation.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/-post-data.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/content.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/id.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-variables.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/data.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query-subscription/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/query.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/-comment.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/content.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/id.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-post.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/comments.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/content.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-result.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/post.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/-builder.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/build.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/id.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-variables.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/build.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/builder.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/id.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/create-post.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/get-post.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/subscribe.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/update.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/decode-result.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/encode-variables.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/data-connect.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/execute.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-data-connect-exception/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/connect-to-emulator.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/host-name.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/port.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/ssl-enabled.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/defaults.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/builder.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/equals.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/hash-code.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/host-name.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/port.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/ssl-enabled.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/to-string.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/data-connect.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/data-connect.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/app.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/close.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/location.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/mutations.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/queries.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/service.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/settings.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/to-string.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/update-settings.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/errors.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-d-e-b-u-g/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-i-n-f-o/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-w-a-r-n-i-n-g/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/entries.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/value-of.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/values.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/-mutation-ref.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/execute.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-network-transport-exception/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/-query-ref.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/execute.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/subscribe.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/flow.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/last-result.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/reload.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/update.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/variables.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-result-decode-exception/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/index.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/log-level.md delete mode 100644 firebase-dataconnect/ktdoc/firebase-dataconnect/package-list delete mode 100644 firebase-dataconnect/ktdoc/index.md diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/decode-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/decode-result.md deleted file mode 100644 index 411b4365e98..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/decode-result.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[BaseRef](../index.md)/[Codec](index.md)/[decodeResult](decode-result.md) - -# decodeResult - -[androidJvm]\ -abstract fun [decodeResult](decode-result.md)(map: [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?>): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/encode-variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/encode-variables.md deleted file mode 100644 index 9b968ef204c..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/encode-variables.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[BaseRef](../index.md)/[Codec](index.md)/[encodeVariables](encode-variables.md) - -# encodeVariables - -[androidJvm]\ -abstract fun [encodeVariables](encode-variables.md)(variables: [VariablesType](index.md)): [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/index.md deleted file mode 100644 index 4228102b68f..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/index.md +++ /dev/null @@ -1,13 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[BaseRef](../index.md)/[Codec](index.md) - -# Codec - -[androidJvm]\ -interface [Codec](index.md)<[VariablesType](index.md), [ResultType](index.md)> - -## Functions - -| Name | Summary | -|---|---| -| [decodeResult](decode-result.md) | [androidJvm]
abstract fun [decodeResult](decode-result.md)(map: [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?>): [ResultType](index.md) | -| [encodeVariables](encode-variables.md) | [androidJvm]
abstract fun [encodeVariables](encode-variables.md)(variables: [VariablesType](index.md)): [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/data-connect.md deleted file mode 100644 index 906a5aa7a69..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/data-connect.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[BaseRef](index.md)/[dataConnect](data-connect.md) - -# dataConnect - -[androidJvm]\ -val [dataConnect](data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/execute.md deleted file mode 100644 index 9982802b47c..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/execute.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[BaseRef](index.md)/[execute](execute.md) - -# execute - -[androidJvm]\ -abstract suspend fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/index.md deleted file mode 100644 index c65636aa336..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/index.md +++ /dev/null @@ -1,29 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[BaseRef](index.md) - -# BaseRef - -abstract class [BaseRef](index.md)<[VariablesType](index.md), [ResultType](index.md)> - -#### Inheritors - -| | -|---| -| [QueryRef](../-query-ref/index.md) | - -## Types - -| Name | Summary | -|---|---| -| [Codec](-codec/index.md) | [androidJvm]
interface [Codec](-codec/index.md)<[VariablesType](-codec/index.md), [ResultType](-codec/index.md)> | - -## Properties - -| Name | Summary | -|---|---| -| [dataConnect](data-connect.md) | [androidJvm]
val [dataConnect](data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) | - -## Functions - -| Name | Summary | -|---|---| -| [execute](execute.md) | [androidJvm]
abstract suspend fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-data-connect-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-data-connect-exception/index.md deleted file mode 100644 index b2b95719497..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-data-connect-exception/index.md +++ /dev/null @@ -1,33 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[DataConnectException](index.md) - -# DataConnectException - -open class [DataConnectException](index.md) : [Exception](https://developer.android.com/reference/kotlin/java/lang/Exception.html) - -#### Inheritors - -| | -|---| -| [NetworkTransportException](../-network-transport-exception/index.md) | -| [ExecutionException](../-execution-exception/index.md) | -| [ResultDecodeException](../-result-decode-exception/index.md) | - -## Properties - -| Name | Summary | -|---|---| -| [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | -| [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | - -## Functions - -| Name | Summary | -|---|---| -| [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | -| [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | -| [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | -| [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | -| [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-execution-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-execution-exception/index.md deleted file mode 100644 index 7a864c2f0ea..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-execution-exception/index.md +++ /dev/null @@ -1,26 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[ExecutionException](index.md) - -# ExecutionException - -[androidJvm]\ -open class [ExecutionException](index.md) : [DataConnectException](../-data-connect-exception/index.md) - -## Properties - -| Name | Summary | -|---|---| -| [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | -| [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | - -## Functions - -| Name | Summary | -|---|---| -| [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | -| [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | -| [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | -| [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | -| [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-firebase-data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-firebase-data-connect.md deleted file mode 100644 index bb2ccf24f3e..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-firebase-data-connect.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[FirebaseDataConnect](index.md)/[FirebaseDataConnect](-firebase-data-connect.md) - -# FirebaseDataConnect - -[androidJvm]\ -constructor() diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/data-connect.md deleted file mode 100644 index 54332a376ab..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/data-connect.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[FirebaseDataConnect](../index.md)/[Queries](index.md)/[dataConnect](data-connect.md) - -# dataConnect - -[androidJvm]\ -val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/index.md deleted file mode 100644 index 5f8e52dc795..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/index.md +++ /dev/null @@ -1,13 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[FirebaseDataConnect](../index.md)/[Queries](index.md) - -# Queries - -[androidJvm]\ -class [Queries](index.md) - -## Properties - -| Name | Summary | -|---|---| -| [dataConnect](data-connect.md) | [androidJvm]
val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) | -| [getPost](../../get-post.md) | [androidJvm]
val [FirebaseDataConnect.Queries](index.md).[getPost](../../get-post.md): [QueryRef](../../-query-ref/index.md)<[GetPostQuery.Variables](../../-get-post-query/-variables/index.md), [GetPostQuery.Result](../../-get-post-query/-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/index.md deleted file mode 100644 index faf9a3e2e26..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/index.md +++ /dev/null @@ -1,24 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[FirebaseDataConnect](index.md) - -# FirebaseDataConnect - -[androidJvm]\ -class [FirebaseDataConnect](index.md) - -## Constructors - -| | | -|---|---| -| [FirebaseDataConnect](-firebase-data-connect.md) | [androidJvm]
constructor() | - -## Types - -| Name | Summary | -|---|---| -| [Queries](-queries/index.md) | [androidJvm]
class [Queries](-queries/index.md) | - -## Properties - -| Name | Summary | -|---|---| -| [queries](queries.md) | [androidJvm]
val [queries](queries.md): [FirebaseDataConnect.Queries](-queries/index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/queries.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/queries.md deleted file mode 100644 index 7c18a83196c..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/queries.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[FirebaseDataConnect](index.md)/[queries](queries.md) - -# queries - -[androidJvm]\ -val [queries](queries.md): [FirebaseDataConnect.Queries](-queries/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-ref/index.md deleted file mode 100644 index b19425f7bf4..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-ref/index.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[GetPostQueryRef](index.md) - -# GetPostQueryRef - -[androidJvm]\ -typealias [GetPostQueryRef](index.md) = [QueryRef](../-query-ref/index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-subscription/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-subscription/index.md deleted file mode 100644 index 6474c7b7267..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-subscription/index.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[GetPostQuerySubscription](index.md) - -# GetPostQuerySubscription - -[androidJvm]\ -typealias [GetPostQuerySubscription](index.md) = [QuerySubscription](../-query-subscription/index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/index.md deleted file mode 100644 index e9c08b7ebb3..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/index.md +++ /dev/null @@ -1,12 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Companion](index.md) - -# Companion - -[androidJvm]\ -object [Companion](index.md) - -## Functions - -| Name | Summary | -|---|---| -| [query](query.md) | [androidJvm]
fun [query](query.md)(dataConnect: [FirebaseDataConnect](../../-firebase-data-connect/index.md)): [QueryRef](../../-query-ref/index.md)<[GetPostQuery.Variables](../-variables/index.md), [GetPostQuery.Result](../-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/query.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/query.md deleted file mode 100644 index 0b198fea4df..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/query.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Companion](index.md)/[query](query.md) - -# query - -[androidJvm]\ -fun [query](query.md)(dataConnect: [FirebaseDataConnect](../../-firebase-data-connect/index.md)): [QueryRef](../../-query-ref/index.md)<[GetPostQuery.Variables](../-variables/index.md), [GetPostQuery.Result](../-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/-comment.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/-comment.md deleted file mode 100644 index f6166547ca1..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/-comment.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[Comment](-comment.md) - -# Comment - -[androidJvm]\ -constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/content.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/content.md deleted file mode 100644 index bead6b5682e..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/content.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[content](content.md) - -# content - -[androidJvm]\ -val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/id.md deleted file mode 100644 index 21255336892..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/id.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[id](id.md) - -# id - -[androidJvm]\ -val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/index.md deleted file mode 100644 index 6084454e818..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/index.md +++ /dev/null @@ -1,19 +0,0 @@ -//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md) - -# Comment - -[androidJvm]\ -data class [Comment](index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) - -## Constructors - -| | | -|---|---| -| [Comment](-comment.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | - -## Properties - -| Name | Summary | -|---|---| -| [content](content.md) | [androidJvm]
val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [id](id.md) | [androidJvm]
val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-post.md deleted file mode 100644 index 4b850ea32da..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-post.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[Post](-post.md) - -# Post - -[androidJvm]\ -constructor(content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/comments.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/comments.md deleted file mode 100644 index 404f4097351..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/comments.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[comments](comments.md) - -# comments - -[androidJvm]\ -val [comments](comments.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/content.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/content.md deleted file mode 100644 index f1eb14144b0..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/content.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[content](content.md) - -# content - -[androidJvm]\ -val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/index.md deleted file mode 100644 index cbb3f7cd8ac..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/index.md +++ /dev/null @@ -1,25 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md) - -# Post - -[androidJvm]\ -data class [Post](index.md)(val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) - -## Constructors - -| | | -|---|---| -| [Post](-post.md) | [androidJvm]
constructor(content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) | - -## Types - -| Name | Summary | -|---|---| -| [Comment](-comment/index.md) | [androidJvm]
data class [Comment](-comment/index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | - -## Properties - -| Name | Summary | -|---|---| -| [comments](comments.md) | [androidJvm]
val [comments](comments.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)> | -| [content](content.md) | [androidJvm]
val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-result.md deleted file mode 100644 index ec4a63a3023..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-result.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md)/[Result](-result.md) - -# Result - -[androidJvm]\ -constructor(post: [GetPostQuery.Result.Post](-post/index.md)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/index.md deleted file mode 100644 index 655d32fb10a..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/index.md +++ /dev/null @@ -1,24 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md) - -# Result - -[androidJvm]\ -data class [Result](index.md)(val post: [GetPostQuery.Result.Post](-post/index.md)) - -## Constructors - -| | | -|---|---| -| [Result](-result.md) | [androidJvm]
constructor(post: [GetPostQuery.Result.Post](-post/index.md)) | - -## Types - -| Name | Summary | -|---|---| -| [Post](-post/index.md) | [androidJvm]
data class [Post](-post/index.md)(val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-post/-comment/index.md)>) | - -## Properties - -| Name | Summary | -|---|---| -| [post](post.md) | [androidJvm]
val [post](post.md): [GetPostQuery.Result.Post](-post/index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/post.md deleted file mode 100644 index c557b5cd578..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/post.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md)/[post](post.md) - -# post - -[androidJvm]\ -val [post](post.md): [GetPostQuery.Result.Post](-post/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/-variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/-variables.md deleted file mode 100644 index f17d267a735..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/-variables.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[Variables](-variables.md) - -# Variables - -[androidJvm]\ -constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/id.md deleted file mode 100644 index b551b6258be..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/id.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[id](id.md) - -# id - -[androidJvm]\ -val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/index.md deleted file mode 100644 index 59408401fe4..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/index.md +++ /dev/null @@ -1,18 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md) - -# Variables - -[androidJvm]\ -data class [Variables](index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) - -## Constructors - -| | | -|---|---| -| [Variables](-variables.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | - -## Properties - -| Name | Summary | -|---|---| -| [id](id.md) | [androidJvm]
val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/index.md deleted file mode 100644 index 720e76ae486..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/index.md +++ /dev/null @@ -1,14 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[GetPostQuery](index.md) - -# GetPostQuery - -[androidJvm]\ -class [GetPostQuery](index.md) - -## Types - -| Name | Summary | -|---|---| -| [Companion](-companion/index.md) | [androidJvm]
object [Companion](-companion/index.md) | -| [Result](-result/index.md) | [androidJvm]
data class [Result](-result/index.md)(val post: [GetPostQuery.Result.Post](-result/-post/index.md)) | -| [Variables](-variables/index.md) | [androidJvm]
data class [Variables](-variables/index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-network-transport-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-network-transport-exception/index.md deleted file mode 100644 index 5fb7ed09dfb..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-network-transport-exception/index.md +++ /dev/null @@ -1,26 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[NetworkTransportException](index.md) - -# NetworkTransportException - -[androidJvm]\ -open class [NetworkTransportException](index.md) : [DataConnectException](../-data-connect-exception/index.md) - -## Properties - -| Name | Summary | -|---|---| -| [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | -| [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | - -## Functions - -| Name | Summary | -|---|---| -| [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | -| [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | -| [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | -| [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | -| [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/-query-ref.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/-query-ref.md deleted file mode 100644 index f721c1410bd..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/-query-ref.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QueryRef](index.md)/[QueryRef](-query-ref.md) - -# QueryRef - -[androidJvm]\ -constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/execute.md deleted file mode 100644 index 36699e66c39..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/execute.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QueryRef](index.md)/[execute](execute.md) - -# execute - -[androidJvm]\ -open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/index.md deleted file mode 100644 index fc782790e26..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/index.md +++ /dev/null @@ -1,27 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QueryRef](index.md) - -# QueryRef - -[androidJvm]\ -class [QueryRef](index.md)<[VariablesType](index.md), [ResultType](index.md)>(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) : [BaseRef](../-base-ref/index.md)<[VariablesType](index.md), [ResultType](index.md)> - -## Constructors - -| | | -|---|---| -| [QueryRef](-query-ref.md) | [androidJvm]
constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) | - -## Properties - -| Name | Summary | -|---|---| -| [dataConnect](../-base-ref/data-connect.md) | [androidJvm]
val [dataConnect](../-base-ref/data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) | - -## Functions - -| Name | Summary | -|---|---| -| [execute](execute.md) | [androidJvm]
open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) | -| [execute](../execute.md) | [androidJvm]
suspend fun [QueryRef](index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)>.[execute](../execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](../-get-post-query/-result/index.md) | -| [subscribe](subscribe.md) | [androidJvm]
fun [subscribe](subscribe.md)(variables: [VariablesType](index.md)): [QuerySubscription](../-query-subscription/index.md)<[VariablesType](index.md), [ResultType](index.md)> | -| [subscribe](../subscribe.md) | [androidJvm]
fun [QueryRef](index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)>.[subscribe](../subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](../-query-subscription/index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/subscribe.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/subscribe.md deleted file mode 100644 index 96fa8b4a7be..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/subscribe.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QueryRef](index.md)/[subscribe](subscribe.md) - -# subscribe - -[androidJvm]\ -fun [subscribe](subscribe.md)(variables: [VariablesType](index.md)): [QuerySubscription](../-query-subscription/index.md)<[VariablesType](index.md), [ResultType](index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/flow.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/flow.md deleted file mode 100644 index 17282ea3507..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/flow.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md)/[flow](flow.md) - -# flow - -[androidJvm]\ -val [flow](flow.md): Flow<[Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/index.md deleted file mode 100644 index aad5e137ebe..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/index.md +++ /dev/null @@ -1,21 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md) - -# QuerySubscription - -[androidJvm]\ -class [QuerySubscription](index.md)<[VariablesType](index.md), [ResultType](index.md)> - -## Properties - -| Name | Summary | -|---|---| -| [flow](flow.md) | [androidJvm]
val [flow](flow.md): Flow<[Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>> | -| [lastResult](last-result.md) | [androidJvm]
val [lastResult](last-result.md): [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>? | -| [query](query.md) | [androidJvm]
val [query](query.md): [QueryRef](../-query-ref/index.md)<[VariablesType](index.md), [ResultType](index.md)> | -| [variables](variables.md) | [androidJvm]
val [variables](variables.md): [VariablesType](index.md) | - -## Functions - -| Name | Summary | -|---|---| -| [reload](reload.md) | [androidJvm]
fun [reload](reload.md)() | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/last-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/last-result.md deleted file mode 100644 index b807559e0c7..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/last-result.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md)/[lastResult](last-result.md) - -# lastResult - -[androidJvm]\ -val [lastResult](last-result.md): [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>? diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/query.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/query.md deleted file mode 100644 index 6ef19668a8a..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/query.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md)/[query](query.md) - -# query - -[androidJvm]\ -val [query](query.md): [QueryRef](../-query-ref/index.md)<[VariablesType](index.md), [ResultType](index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/reload.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/reload.md deleted file mode 100644 index 88b741acda2..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/reload.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md)/[reload](reload.md) - -# reload - -[androidJvm]\ -fun [reload](reload.md)() diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/variables.md deleted file mode 100644 index aada19de289..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/variables.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[QuerySubscription](index.md)/[variables](variables.md) - -# variables - -[androidJvm]\ -val [variables](variables.md): [VariablesType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md deleted file mode 100644 index 5681bc38bc6..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md +++ /dev/null @@ -1,26 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.apiproposal](../index.md)/[ResultDecodeException](index.md) - -# ResultDecodeException - -[androidJvm]\ -open class [ResultDecodeException](index.md) : [DataConnectException](../-data-connect-exception/index.md) - -## Properties - -| Name | Summary | -|---|---| -| [cause](index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | -| [message](index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | - -## Functions - -| Name | Summary | -|---|---| -| [addSuppressed](index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | -| [fillInStackTrace](index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [getLocalizedMessage](index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [getStackTrace](index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | -| [getSuppressed](index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | -| [initCause](index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [printStackTrace](index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | -| [setStackTrace](index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/execute.md deleted file mode 100644 index ff24d732e34..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/execute.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.apiproposal](index.md)/[execute](execute.md) - -# execute - -[androidJvm]\ -suspend fun [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](-get-post-query/-result/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/get-post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/get-post.md deleted file mode 100644 index a1c0add3c84..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/get-post.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.apiproposal](index.md)/[getPost](get-post.md) - -# getPost - -[androidJvm]\ -val [FirebaseDataConnect.Queries](-firebase-data-connect/-queries/index.md).[getPost](get-post.md): [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md deleted file mode 100644 index 282a8e624c9..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md +++ /dev/null @@ -1,32 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.apiproposal](index.md) - -# Package-level declarations - -## Types - -| Name | Summary | -|---|---| -| [BaseRef](-base-ref/index.md) | [androidJvm]
abstract class [BaseRef](-base-ref/index.md)<[VariablesType](-base-ref/index.md), [ResultType](-base-ref/index.md)> | -| [DataConnectException](-data-connect-exception/index.md) | [androidJvm]
open class [DataConnectException](-data-connect-exception/index.md) : [Exception](https://developer.android.com/reference/kotlin/java/lang/Exception.html) | -| [ExecutionException](-execution-exception/index.md) | [androidJvm]
open class [ExecutionException](-execution-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | -| [FirebaseDataConnect](-firebase-data-connect/index.md) | [androidJvm]
class [FirebaseDataConnect](-firebase-data-connect/index.md) | -| [GetPostQuery](-get-post-query/index.md) | [androidJvm]
class [GetPostQuery](-get-post-query/index.md) | -| [GetPostQueryRef](-get-post-query-ref/index.md) | [androidJvm]
typealias [GetPostQueryRef](-get-post-query-ref/index.md) = [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | -| [GetPostQuerySubscription](-get-post-query-subscription/index.md) | [androidJvm]
typealias [GetPostQuerySubscription](-get-post-query-subscription/index.md) = [QuerySubscription](-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | -| [NetworkTransportException](-network-transport-exception/index.md) | [androidJvm]
open class [NetworkTransportException](-network-transport-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | -| [QueryRef](-query-ref/index.md) | [androidJvm]
class [QueryRef](-query-ref/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)>(dataConnect: [FirebaseDataConnect](-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](-base-ref/-codec/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)>) : [BaseRef](-base-ref/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)> | -| [QuerySubscription](-query-subscription/index.md) | [androidJvm]
class [QuerySubscription](-query-subscription/index.md)<[VariablesType](-query-subscription/index.md), [ResultType](-query-subscription/index.md)> | -| [ResultDecodeException](-result-decode-exception/index.md) | [androidJvm]
open class [ResultDecodeException](-result-decode-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | - -## Properties - -| Name | Summary | -|---|---| -| [getPost](get-post.md) | [androidJvm]
val [FirebaseDataConnect.Queries](-firebase-data-connect/-queries/index.md).[getPost](get-post.md): [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | - -## Functions - -| Name | Summary | -|---|---| -| [execute](execute.md) | [androidJvm]
suspend fun [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](-get-post-query/-result/index.md) | -| [subscribe](subscribe.md) | [androidJvm]
fun [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[subscribe](subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/subscribe.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/subscribe.md deleted file mode 100644 index 7ecca5478a8..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/subscribe.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.apiproposal](index.md)/[subscribe](subscribe.md) - -# subscribe - -[androidJvm]\ -fun [QueryRef](-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[subscribe](subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/index.md deleted file mode 100644 index f060f0a67ab..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/index.md +++ /dev/null @@ -1,12 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[CreatePostMutation](../index.md)/[Companion](index.md) - -# Companion - -[androidJvm]\ -object [Companion](index.md) - -## Functions - -| Name | Summary | -|---|---| -| [mutation](mutation.md) | [androidJvm]
fun [mutation](mutation.md)(dataConnect: [FirebaseDataConnect](../../../com.google.firebase.dataconnect/-firebase-data-connect/index.md)): [MutationRef](../../../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](../-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/mutation.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/mutation.md deleted file mode 100644 index 98797225697..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/mutation.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[CreatePostMutation](../index.md)/[Companion](index.md)/[mutation](mutation.md) - -# mutation - -[androidJvm]\ -fun [mutation](mutation.md)(dataConnect: [FirebaseDataConnect](../../../com.google.firebase.dataconnect/-firebase-data-connect/index.md)): [MutationRef](../../../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](../-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/-post-data.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/-post-data.md deleted file mode 100644 index aec589f281b..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/-post-data.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[CreatePostMutation](../../index.md)/[Variables](../index.md)/[PostData](index.md)/[PostData](-post-data.md) - -# PostData - -[androidJvm]\ -constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/content.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/content.md deleted file mode 100644 index 6b2aa04a7a3..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/content.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[CreatePostMutation](../../index.md)/[Variables](../index.md)/[PostData](index.md)/[content](content.md) - -# content - -[androidJvm]\ -val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/id.md deleted file mode 100644 index 65a108b9366..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/id.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[CreatePostMutation](../../index.md)/[Variables](../index.md)/[PostData](index.md)/[id](id.md) - -# id - -[androidJvm]\ -val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/index.md deleted file mode 100644 index baf513ae85f..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/index.md +++ /dev/null @@ -1,19 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[CreatePostMutation](../../index.md)/[Variables](../index.md)/[PostData](index.md) - -# PostData - -[androidJvm]\ -data class [PostData](index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) - -## Constructors - -| | | -|---|---| -| [PostData](-post-data.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | - -## Properties - -| Name | Summary | -|---|---| -| [content](content.md) | [androidJvm]
val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [id](id.md) | [androidJvm]
val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-variables.md deleted file mode 100644 index ee7b77d437f..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-variables.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[CreatePostMutation](../index.md)/[Variables](index.md)/[Variables](-variables.md) - -# Variables - -[androidJvm]\ -constructor(data: [CreatePostMutation.Variables.PostData](-post-data/index.md)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/data.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/data.md deleted file mode 100644 index 601fade12b4..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/data.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[CreatePostMutation](../index.md)/[Variables](index.md)/[data](data.md) - -# data - -[androidJvm]\ -val [data](data.md): [CreatePostMutation.Variables.PostData](-post-data/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md deleted file mode 100644 index 9120d5def22..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md +++ /dev/null @@ -1,24 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[CreatePostMutation](../index.md)/[Variables](index.md) - -# Variables - -[androidJvm]\ -data class [Variables](index.md)(val data: [CreatePostMutation.Variables.PostData](-post-data/index.md)) - -## Constructors - -| | | -|---|---| -| [Variables](-variables.md) | [androidJvm]
constructor(data: [CreatePostMutation.Variables.PostData](-post-data/index.md)) | - -## Types - -| Name | Summary | -|---|---| -| [PostData](-post-data/index.md) | [androidJvm]
data class [PostData](-post-data/index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | - -## Properties - -| Name | Summary | -|---|---| -| [data](data.md) | [androidJvm]
val [data](data.md): [CreatePostMutation.Variables.PostData](-post-data/index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/index.md deleted file mode 100644 index a6d504d95ac..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/index.md +++ /dev/null @@ -1,13 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.generated](../index.md)/[CreatePostMutation](index.md) - -# CreatePostMutation - -[androidJvm]\ -class [CreatePostMutation](index.md) - -## Types - -| Name | Summary | -|---|---| -| [Companion](-companion/index.md) | [androidJvm]
object [Companion](-companion/index.md) | -| [Variables](-variables/index.md) | [androidJvm]
data class [Variables](-variables/index.md)(val data: [CreatePostMutation.Variables.PostData](-variables/-post-data/index.md)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query-subscription/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query-subscription/index.md deleted file mode 100644 index b858484ab7b..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query-subscription/index.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.generated](../index.md)/[GetPostQuerySubscription](index.md) - -# GetPostQuerySubscription - -[androidJvm]\ -typealias [GetPostQuerySubscription](index.md) = [QuerySubscription](../../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](../-get-post-query/-variables/index.md), [GetPostQuery.Result](../-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/index.md deleted file mode 100644 index 386d1959656..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/index.md +++ /dev/null @@ -1,12 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Companion](index.md) - -# Companion - -[androidJvm]\ -object [Companion](index.md) - -## Functions - -| Name | Summary | -|---|---| -| [query](query.md) | [androidJvm]
fun [query](query.md)(dataConnect: [FirebaseDataConnect](../../../com.google.firebase.dataconnect/-firebase-data-connect/index.md)): [QueryRef](../../../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](../-variables/index.md), [GetPostQuery.Result](../-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/query.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/query.md deleted file mode 100644 index e01a0937810..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/query.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Companion](index.md)/[query](query.md) - -# query - -[androidJvm]\ -fun [query](query.md)(dataConnect: [FirebaseDataConnect](../../../com.google.firebase.dataconnect/-firebase-data-connect/index.md)): [QueryRef](../../../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](../-variables/index.md), [GetPostQuery.Result](../-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/-comment.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/-comment.md deleted file mode 100644 index 7853de8c678..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/-comment.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[Comment](-comment.md) - -# Comment - -[androidJvm]\ -constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/content.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/content.md deleted file mode 100644 index b1e4570df0e..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/content.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[content](content.md) - -# content - -[androidJvm]\ -val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/id.md deleted file mode 100644 index ff063410da2..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/id.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md)/[id](id.md) - -# id - -[androidJvm]\ -val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/index.md deleted file mode 100644 index 1f2f7c8bcd4..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/index.md +++ /dev/null @@ -1,19 +0,0 @@ -//[firebase-dataconnect](../../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../../index.md)/[GetPostQuery](../../../index.md)/[Result](../../index.md)/[Post](../index.md)/[Comment](index.md) - -# Comment - -[androidJvm]\ -data class [Comment](index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) - -## Constructors - -| | | -|---|---| -| [Comment](-comment.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | - -## Properties - -| Name | Summary | -|---|---| -| [content](content.md) | [androidJvm]
val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [id](id.md) | [androidJvm]
val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-post.md deleted file mode 100644 index 917b60a352a..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-post.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[Post](-post.md) - -# Post - -[androidJvm]\ -constructor(content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/comments.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/comments.md deleted file mode 100644 index b1f5d4e2968..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/comments.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[comments](comments.md) - -# comments - -[androidJvm]\ -val [comments](comments.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/content.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/content.md deleted file mode 100644 index f09ff4af0ce..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/content.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md)/[content](content.md) - -# content - -[androidJvm]\ -val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/index.md deleted file mode 100644 index f375dff63ee..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/index.md +++ /dev/null @@ -1,25 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Result](../index.md)/[Post](index.md) - -# Post - -[androidJvm]\ -data class [Post](index.md)(val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) - -## Constructors - -| | | -|---|---| -| [Post](-post.md) | [androidJvm]
constructor(content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)>) | - -## Types - -| Name | Summary | -|---|---| -| [Comment](-comment/index.md) | [androidJvm]
data class [Comment](-comment/index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | - -## Properties - -| Name | Summary | -|---|---| -| [comments](comments.md) | [androidJvm]
val [comments](comments.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-comment/index.md)> | -| [content](content.md) | [androidJvm]
val [content](content.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-result.md deleted file mode 100644 index 4f9b1ebcf4d..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-result.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md)/[Result](-result.md) - -# Result - -[androidJvm]\ -constructor(post: [GetPostQuery.Result.Post](-post/index.md)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md deleted file mode 100644 index ff0627369cc..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md +++ /dev/null @@ -1,24 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md) - -# Result - -[androidJvm]\ -data class [Result](index.md)(val post: [GetPostQuery.Result.Post](-post/index.md)) - -## Constructors - -| | | -|---|---| -| [Result](-result.md) | [androidJvm]
constructor(post: [GetPostQuery.Result.Post](-post/index.md)) | - -## Types - -| Name | Summary | -|---|---| -| [Post](-post/index.md) | [androidJvm]
data class [Post](-post/index.md)(val content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), val comments: [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[GetPostQuery.Result.Post.Comment](-post/-comment/index.md)>) | - -## Properties - -| Name | Summary | -|---|---| -| [post](post.md) | [androidJvm]
val [post](post.md): [GetPostQuery.Result.Post](-post/index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/post.md deleted file mode 100644 index 5c0473c0a8a..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/post.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Result](index.md)/[post](post.md) - -# post - -[androidJvm]\ -val [post](post.md): [GetPostQuery.Result.Post](-post/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/-builder.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/-builder.md deleted file mode 100644 index 028149df152..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/-builder.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Variables](../index.md)/[Builder](index.md)/[Builder](-builder.md) - -# Builder - -[androidJvm]\ -constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/build.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/build.md deleted file mode 100644 index 1491e88aa66..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/build.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Variables](../index.md)/[Builder](index.md)/[build](build.md) - -# build - -[androidJvm]\ -fun [build](build.md)(): [GetPostQuery.Variables](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/id.md deleted file mode 100644 index f7e8c9a6527..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/id.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Variables](../index.md)/[Builder](index.md)/[id](id.md) - -# id - -[androidJvm]\ -var [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md deleted file mode 100644 index 105626cc0bd..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md +++ /dev/null @@ -1,24 +0,0 @@ -//[firebase-dataconnect](../../../../../index.md)/[com.google.firebase.dataconnect.generated](../../../index.md)/[GetPostQuery](../../index.md)/[Variables](../index.md)/[Builder](index.md) - -# Builder - -[androidJvm]\ -class [Builder](index.md)(var id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) - -## Constructors - -| | | -|---|---| -| [Builder](-builder.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | - -## Properties - -| Name | Summary | -|---|---| -| [id](id.md) | [androidJvm]
var [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | - -## Functions - -| Name | Summary | -|---|---| -| [build](build.md) | [androidJvm]
fun [build](build.md)(): [GetPostQuery.Variables](../index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-variables.md deleted file mode 100644 index c67ae8feaea..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-variables.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[Variables](-variables.md) - -# Variables - -[androidJvm]\ -constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/build.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/build.md deleted file mode 100644 index 4cf69611ea1..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/build.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[build](build.md) - -# build - -[androidJvm]\ -fun [build](build.md)(block: [GetPostQuery.Variables.Builder](-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)): [GetPostQuery.Variables](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/builder.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/builder.md deleted file mode 100644 index f36b3df7a56..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/builder.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[builder](builder.md) - -# builder - -[androidJvm]\ -val [builder](builder.md): [GetPostQuery.Variables.Builder](-builder/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/id.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/id.md deleted file mode 100644 index cefd68ad494..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/id.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md)/[id](id.md) - -# id - -[androidJvm]\ -val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md deleted file mode 100644 index b2876271cc3..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md +++ /dev/null @@ -1,31 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect.generated](../../index.md)/[GetPostQuery](../index.md)/[Variables](index.md) - -# Variables - -[androidJvm]\ -data class [Variables](index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) - -## Constructors - -| | | -|---|---| -| [Variables](-variables.md) | [androidJvm]
constructor(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | - -## Types - -| Name | Summary | -|---|---| -| [Builder](-builder/index.md) | [androidJvm]
class [Builder](-builder/index.md)(var id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | - -## Properties - -| Name | Summary | -|---|---| -| [builder](builder.md) | [androidJvm]
val [builder](builder.md): [GetPostQuery.Variables.Builder](-builder/index.md) | -| [id](id.md) | [androidJvm]
val [id](id.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | - -## Functions - -| Name | Summary | -|---|---| -| [build](build.md) | [androidJvm]
fun [build](build.md)(block: [GetPostQuery.Variables.Builder](-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)): [GetPostQuery.Variables](index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/index.md deleted file mode 100644 index c9fc41fce8f..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/index.md +++ /dev/null @@ -1,14 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect.generated](../index.md)/[GetPostQuery](index.md) - -# GetPostQuery - -[androidJvm]\ -class [GetPostQuery](index.md) - -## Types - -| Name | Summary | -|---|---| -| [Companion](-companion/index.md) | [androidJvm]
object [Companion](-companion/index.md) | -| [Result](-result/index.md) | [androidJvm]
data class [Result](-result/index.md)(val post: [GetPostQuery.Result.Post](-result/-post/index.md)) | -| [Variables](-variables/index.md) | [androidJvm]
data class [Variables](-variables/index.md)(val id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/create-post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/create-post.md deleted file mode 100644 index 7b0f285eedf..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/create-post.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md)/[createPost](create-post.md) - -# createPost - -[androidJvm]\ -val [FirebaseDataConnect.Mutations](../com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md).[createPost](create-post.md): [MutationRef](../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md deleted file mode 100644 index 476dec921ed..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md +++ /dev/null @@ -1,8 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md)/[execute](execute.md) - -# execute - -[androidJvm]\ -suspend fun [MutationRef](../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) - -suspend fun [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](-get-post-query/-result/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/get-post.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/get-post.md deleted file mode 100644 index 54d2d69b7f9..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/get-post.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md)/[getPost](get-post.md) - -# getPost - -[androidJvm]\ -val [FirebaseDataConnect.Queries](../com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md).[getPost](get-post.md): [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md deleted file mode 100644 index 4768ecb4b5c..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md +++ /dev/null @@ -1,26 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md) - -# Package-level declarations - -## Types - -| Name | Summary | -|---|---| -| [CreatePostMutation](-create-post-mutation/index.md) | [androidJvm]
class [CreatePostMutation](-create-post-mutation/index.md) | -| [GetPostQuery](-get-post-query/index.md) | [androidJvm]
class [GetPostQuery](-get-post-query/index.md) | -| [GetPostQuerySubscription](-get-post-query-subscription/index.md) | [androidJvm]
typealias [GetPostQuerySubscription](-get-post-query-subscription/index.md) = [QuerySubscription](../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | - -## Properties - -| Name | Summary | -|---|---| -| [createPost](create-post.md) | [androidJvm]
val [FirebaseDataConnect.Mutations](../com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md).[createPost](create-post.md): [MutationRef](../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)> | -| [getPost](get-post.md) | [androidJvm]
val [FirebaseDataConnect.Queries](../com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md).[getPost](get-post.md): [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | - -## Functions - -| Name | Summary | -|---|---| -| [execute](execute.md) | [androidJvm]
suspend fun [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](-get-post-query/-result/index.md)
suspend fun [MutationRef](../com.google.firebase.dataconnect/-mutation-ref/index.md)<[CreatePostMutation.Variables](-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)>.[execute](execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | -| [subscribe](subscribe.md) | [androidJvm]
fun [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[subscribe](subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> | -| [update](update.md) | [androidJvm]
fun [QuerySubscription](../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[update](update.md)(block: [GetPostQuery.Variables.Builder](-get-post-query/-variables/-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/subscribe.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/subscribe.md deleted file mode 100644 index e3e8718dda7..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/subscribe.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md)/[subscribe](subscribe.md) - -# subscribe - -[androidJvm]\ -fun [QueryRef](../com.google.firebase.dataconnect/-query-ref/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[subscribe](subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/update.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/update.md deleted file mode 100644 index 9b13324b0ab..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect.generated/update.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect.generated](index.md)/[update](update.md) - -# update - -[androidJvm]\ -fun [QuerySubscription](../com.google.firebase.dataconnect/-query-subscription/index.md)<[GetPostQuery.Variables](-get-post-query/-variables/index.md), [GetPostQuery.Result](-get-post-query/-result/index.md)>.[update](update.md)(block: [GetPostQuery.Variables.Builder](-get-post-query/-variables/-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/decode-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/decode-result.md deleted file mode 100644 index 1e6826ca0e2..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/decode-result.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[BaseRef](../index.md)/[Codec](index.md)/[decodeResult](decode-result.md) - -# decodeResult - -[androidJvm]\ -abstract fun [decodeResult](decode-result.md)(map: [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?>): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/encode-variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/encode-variables.md deleted file mode 100644 index d5c1d9cf534..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/encode-variables.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[BaseRef](../index.md)/[Codec](index.md)/[encodeVariables](encode-variables.md) - -# encodeVariables - -[androidJvm]\ -abstract fun [encodeVariables](encode-variables.md)(variables: [VariablesType](index.md)): [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/index.md deleted file mode 100644 index bbe2ea734f0..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/index.md +++ /dev/null @@ -1,13 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[BaseRef](../index.md)/[Codec](index.md) - -# Codec - -[androidJvm]\ -interface [Codec](index.md)<[VariablesType](index.md), [ResultType](index.md)> - -## Functions - -| Name | Summary | -|---|---| -| [decodeResult](decode-result.md) | [androidJvm]
abstract fun [decodeResult](decode-result.md)(map: [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?>): [ResultType](index.md) | -| [encodeVariables](encode-variables.md) | [androidJvm]
abstract fun [encodeVariables](encode-variables.md)(variables: [VariablesType](index.md)): [Map](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/data-connect.md deleted file mode 100644 index 3d37fb90aab..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/data-connect.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[BaseRef](index.md)/[dataConnect](data-connect.md) - -# dataConnect - -[androidJvm]\ -val [dataConnect](data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/execute.md deleted file mode 100644 index c73926c34e2..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/execute.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[BaseRef](index.md)/[execute](execute.md) - -# execute - -[androidJvm]\ -abstract suspend fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/index.md deleted file mode 100644 index 03e1a146a8e..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/index.md +++ /dev/null @@ -1,30 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[BaseRef](index.md) - -# BaseRef - -abstract class [BaseRef](index.md)<[VariablesType](index.md), [ResultType](index.md)> - -#### Inheritors - -| | -|---| -| [MutationRef](../-mutation-ref/index.md) | -| [QueryRef](../-query-ref/index.md) | - -## Types - -| Name | Summary | -|---|---| -| [Codec](-codec/index.md) | [androidJvm]
interface [Codec](-codec/index.md)<[VariablesType](-codec/index.md), [ResultType](-codec/index.md)> | - -## Properties - -| Name | Summary | -|---|---| -| [dataConnect](data-connect.md) | [androidJvm]
val [dataConnect](data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) | - -## Functions - -| Name | Summary | -|---|---| -| [execute](execute.md) | [androidJvm]
abstract suspend fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-data-connect-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-data-connect-exception/index.md deleted file mode 100644 index 3fc9a1dd3c7..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-data-connect-exception/index.md +++ /dev/null @@ -1,33 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[DataConnectException](index.md) - -# DataConnectException - -open class [DataConnectException](index.md) : [Exception](https://developer.android.com/reference/kotlin/java/lang/Exception.html) - -#### Inheritors - -| | -|---| -| [NetworkTransportException](../-network-transport-exception/index.md) | -| [GraphQLException](../-graph-q-l-exception/index.md) | -| [ResultDecodeException](../-result-decode-exception/index.md) | - -## Properties - -| Name | Summary | -|---|---| -| [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | -| [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | - -## Functions - -| Name | Summary | -|---|---| -| [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | -| [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | -| [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | -| [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | -| [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md deleted file mode 100644 index 387b2854203..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md +++ /dev/null @@ -1,8 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md)/[build](build.md) - -# build - -[androidJvm]\ -fun [build](build.md)(): [FirebaseDataConnectSettings](../index.md) - -fun [build](build.md)(block: [FirebaseDataConnectSettings.Builder](index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)): [FirebaseDataConnectSettings](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/connect-to-emulator.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/connect-to-emulator.md deleted file mode 100644 index e76328becd6..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/connect-to-emulator.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md)/[connectToEmulator](connect-to-emulator.md) - -# connectToEmulator - -[androidJvm]\ -fun [connectToEmulator](connect-to-emulator.md)() diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/host-name.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/host-name.md deleted file mode 100644 index fbbae7811f5..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/host-name.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md)/[hostName](host-name.md) - -# hostName - -[androidJvm]\ -var [hostName](host-name.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/index.md deleted file mode 100644 index c778ca6c917..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/index.md +++ /dev/null @@ -1,21 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md) - -# Builder - -[androidJvm]\ -class [Builder](index.md) - -## Properties - -| Name | Summary | -|---|---| -| [hostName](host-name.md) | [androidJvm]
var [hostName](host-name.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [port](port.md) | [androidJvm]
var [port](port.md): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | -| [sslEnabled](ssl-enabled.md) | [androidJvm]
var [sslEnabled](ssl-enabled.md): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) | - -## Functions - -| Name | Summary | -|---|---| -| [build](build.md) | [androidJvm]
fun [build](build.md)(): [FirebaseDataConnectSettings](../index.md)
fun [build](build.md)(block: [FirebaseDataConnectSettings.Builder](index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)): [FirebaseDataConnectSettings](../index.md) | -| [connectToEmulator](connect-to-emulator.md) | [androidJvm]
fun [connectToEmulator](connect-to-emulator.md)() | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/port.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/port.md deleted file mode 100644 index 30c59011838..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/port.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md)/[port](port.md) - -# port - -[androidJvm]\ -var [port](port.md): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/ssl-enabled.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/ssl-enabled.md deleted file mode 100644 index d29bf91ab1b..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/ssl-enabled.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Builder](index.md)/[sslEnabled](ssl-enabled.md) - -# sslEnabled - -[androidJvm]\ -var [sslEnabled](ssl-enabled.md): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/defaults.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/defaults.md deleted file mode 100644 index f020b3fb29e..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/defaults.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Companion](index.md)/[defaults](defaults.md) - -# defaults - -[androidJvm]\ -val [defaults](defaults.md): [FirebaseDataConnectSettings](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/index.md deleted file mode 100644 index 3ea913736be..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/index.md +++ /dev/null @@ -1,12 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnectSettings](../index.md)/[Companion](index.md) - -# Companion - -[androidJvm]\ -object [Companion](index.md) - -## Properties - -| Name | Summary | -|---|---| -| [defaults](defaults.md) | [androidJvm]
val [defaults](defaults.md): [FirebaseDataConnectSettings](../index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/builder.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/builder.md deleted file mode 100644 index ab802f17164..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/builder.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[builder](builder.md) - -# builder - -[androidJvm]\ -val [builder](builder.md): [FirebaseDataConnectSettings.Builder](-builder/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/equals.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/equals.md deleted file mode 100644 index df0e222aa3b..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/equals.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[equals](equals.md) - -# equals - -[androidJvm]\ -open operator override fun [equals](equals.md)(other: [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/hash-code.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/hash-code.md deleted file mode 100644 index 486ca715356..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/hash-code.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[hashCode](hash-code.md) - -# hashCode - -[androidJvm]\ -open override fun [hashCode](hash-code.md)(): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/host-name.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/host-name.md deleted file mode 100644 index 1c188421ea4..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/host-name.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[hostName](host-name.md) - -# hostName - -[androidJvm]\ -val [hostName](host-name.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/index.md deleted file mode 100644 index 1ed330bf008..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/index.md +++ /dev/null @@ -1,30 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md) - -# FirebaseDataConnectSettings - -[androidJvm]\ -class [FirebaseDataConnectSettings](index.md) - -## Types - -| Name | Summary | -|---|---| -| [Builder](-builder/index.md) | [androidJvm]
class [Builder](-builder/index.md) | -| [Companion](-companion/index.md) | [androidJvm]
object [Companion](-companion/index.md) | - -## Properties - -| Name | Summary | -|---|---| -| [builder](builder.md) | [androidJvm]
val [builder](builder.md): [FirebaseDataConnectSettings.Builder](-builder/index.md) | -| [hostName](host-name.md) | [androidJvm]
val [hostName](host-name.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [port](port.md) | [androidJvm]
val [port](port.md): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | -| [sslEnabled](ssl-enabled.md) | [androidJvm]
val [sslEnabled](ssl-enabled.md): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) | - -## Functions - -| Name | Summary | -|---|---| -| [equals](equals.md) | [androidJvm]
open operator override fun [equals](equals.md)(other: [Any](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-any/index.html)?): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) | -| [hashCode](hash-code.md) | [androidJvm]
open override fun [hashCode](hash-code.md)(): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | -| [toString](to-string.md) | [androidJvm]
open override fun [toString](to-string.md)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/port.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/port.md deleted file mode 100644 index 48f1bffe411..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/port.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[port](port.md) - -# port - -[androidJvm]\ -val [port](port.md): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/ssl-enabled.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/ssl-enabled.md deleted file mode 100644 index 2cfed3000e3..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/ssl-enabled.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[sslEnabled](ssl-enabled.md) - -# sslEnabled - -[androidJvm]\ -val [sslEnabled](ssl-enabled.md): [Boolean](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/to-string.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/to-string.md deleted file mode 100644 index a99556f49c3..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/to-string.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnectSettings](index.md)/[toString](to-string.md) - -# toString - -[androidJvm]\ -open override fun [toString](to-string.md)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md deleted file mode 100644 index 06c3bfaf12d..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md +++ /dev/null @@ -1,8 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Companion](index.md)/[getInstance](get-instance.md) - -# getInstance - -[androidJvm]\ -fun [getInstance](get-instance.md)(location: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), service: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [FirebaseDataConnect](../index.md) - -fun [getInstance](get-instance.md)(app: FirebaseApp, location: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), service: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [FirebaseDataConnect](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/index.md deleted file mode 100644 index feabf7fb6b7..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/index.md +++ /dev/null @@ -1,12 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Companion](index.md) - -# Companion - -[androidJvm]\ -object [Companion](index.md) - -## Functions - -| Name | Summary | -|---|---| -| [getInstance](get-instance.md) | [androidJvm]
fun [getInstance](get-instance.md)(location: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), service: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [FirebaseDataConnect](../index.md)
fun [getInstance](get-instance.md)(app: FirebaseApp, location: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), service: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [FirebaseDataConnect](../index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/data-connect.md deleted file mode 100644 index da50daa6446..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/data-connect.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Mutations](index.md)/[dataConnect](data-connect.md) - -# dataConnect - -[androidJvm]\ -val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md deleted file mode 100644 index 03f8fb66526..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md +++ /dev/null @@ -1,13 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Mutations](index.md) - -# Mutations - -[androidJvm]\ -class [Mutations](index.md) - -## Properties - -| Name | Summary | -|---|---| -| [createPost](../../../com.google.firebase.dataconnect.generated/create-post.md) | [androidJvm]
val [FirebaseDataConnect.Mutations](index.md).[createPost](../../../com.google.firebase.dataconnect.generated/create-post.md): [MutationRef](../../-mutation-ref/index.md)<[CreatePostMutation.Variables](../../../com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)> | -| [dataConnect](data-connect.md) | [androidJvm]
val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/data-connect.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/data-connect.md deleted file mode 100644 index 8666ae0eb1e..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/data-connect.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Queries](index.md)/[dataConnect](data-connect.md) - -# dataConnect - -[androidJvm]\ -val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md deleted file mode 100644 index f70f3dbeb4e..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md +++ /dev/null @@ -1,13 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[FirebaseDataConnect](../index.md)/[Queries](index.md) - -# Queries - -[androidJvm]\ -class [Queries](index.md) - -## Properties - -| Name | Summary | -|---|---| -| [dataConnect](data-connect.md) | [androidJvm]
val [dataConnect](data-connect.md): [FirebaseDataConnect](../index.md) | -| [getPost](../../../com.google.firebase.dataconnect.generated/get-post.md) | [androidJvm]
val [FirebaseDataConnect.Queries](index.md).[getPost](../../../com.google.firebase.dataconnect.generated/get-post.md): [QueryRef](../../-query-ref/index.md)<[GetPostQuery.Variables](../../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md), [GetPostQuery.Result](../../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/app.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/app.md deleted file mode 100644 index 33880164cc1..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/app.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[app](app.md) - -# app - -[androidJvm]\ -val [app](app.md): FirebaseApp diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/close.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/close.md deleted file mode 100644 index ee2dba34c6b..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/close.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[close](close.md) - -# close - -[androidJvm]\ -open override fun [close](close.md)() diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/index.md deleted file mode 100644 index 125525362f8..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/index.md +++ /dev/null @@ -1,33 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md) - -# FirebaseDataConnect - -[androidJvm]\ -class [FirebaseDataConnect](index.md) : [Closeable](https://developer.android.com/reference/kotlin/java/io/Closeable.html) - -## Types - -| Name | Summary | -|---|---| -| [Companion](-companion/index.md) | [androidJvm]
object [Companion](-companion/index.md) | -| [Mutations](-mutations/index.md) | [androidJvm]
class [Mutations](-mutations/index.md) | -| [Queries](-queries/index.md) | [androidJvm]
class [Queries](-queries/index.md) | - -## Properties - -| Name | Summary | -|---|---| -| [app](app.md) | [androidJvm]
val [app](app.md): FirebaseApp | -| [location](location.md) | [androidJvm]
val [location](location.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [mutations](mutations.md) | [androidJvm]
val [mutations](mutations.md): [FirebaseDataConnect.Mutations](-mutations/index.md) | -| [queries](queries.md) | [androidJvm]
val [queries](queries.md): [FirebaseDataConnect.Queries](-queries/index.md) | -| [service](service.md) | [androidJvm]
val [service](service.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [settings](settings.md) | [androidJvm]
var [settings](settings.md): [FirebaseDataConnectSettings](../-firebase-data-connect-settings/index.md) | - -## Functions - -| Name | Summary | -|---|---| -| [close](close.md) | [androidJvm]
open override fun [close](close.md)() | -| [toString](to-string.md) | [androidJvm]
open override fun [toString](to-string.md)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [updateSettings](update-settings.md) | [androidJvm]
fun [updateSettings](update-settings.md)(block: [FirebaseDataConnectSettings.Builder](../-firebase-data-connect-settings/-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/location.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/location.md deleted file mode 100644 index 7db18da669e..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/location.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[location](location.md) - -# location - -[androidJvm]\ -val [location](location.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/mutations.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/mutations.md deleted file mode 100644 index 1b927867f39..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/mutations.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[mutations](mutations.md) - -# mutations - -[androidJvm]\ -val [mutations](mutations.md): [FirebaseDataConnect.Mutations](-mutations/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/queries.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/queries.md deleted file mode 100644 index 8de482ce9bd..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/queries.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[queries](queries.md) - -# queries - -[androidJvm]\ -val [queries](queries.md): [FirebaseDataConnect.Queries](-queries/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/service.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/service.md deleted file mode 100644 index f7e86eb9730..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/service.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[service](service.md) - -# service - -[androidJvm]\ -val [service](service.md): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/settings.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/settings.md deleted file mode 100644 index c787fadc53f..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/settings.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[settings](settings.md) - -# settings - -[androidJvm]\ -var [settings](settings.md): [FirebaseDataConnectSettings](../-firebase-data-connect-settings/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/to-string.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/to-string.md deleted file mode 100644 index 84ce9a4156f..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/to-string.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[toString](to-string.md) - -# toString - -[androidJvm]\ -open override fun [toString](to-string.md)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/update-settings.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/update-settings.md deleted file mode 100644 index 39069967ae6..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/update-settings.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[FirebaseDataConnect](index.md)/[updateSettings](update-settings.md) - -# updateSettings - -[androidJvm]\ -fun [updateSettings](update-settings.md)(block: [FirebaseDataConnectSettings.Builder](../-firebase-data-connect-settings/-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/errors.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/errors.md deleted file mode 100644 index de24560f2b4..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/errors.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[GraphQLException](index.md)/[errors](errors.md) - -# errors - -[androidJvm]\ -val [errors](errors.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/index.md deleted file mode 100644 index 6097c9938b2..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/index.md +++ /dev/null @@ -1,27 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[GraphQLException](index.md) - -# GraphQLException - -[androidJvm]\ -open class [GraphQLException](index.md) : [DataConnectException](../-data-connect-exception/index.md) - -## Properties - -| Name | Summary | -|---|---| -| [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | -| [errors](errors.md) | [androidJvm]
val [errors](errors.md): [List](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/index.html)<[String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)> | -| [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | - -## Functions - -| Name | Summary | -|---|---| -| [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | -| [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | -| [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | -| [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | -| [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-d-e-b-u-g/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-d-e-b-u-g/index.md deleted file mode 100644 index 6a949ed9213..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-d-e-b-u-g/index.md +++ /dev/null @@ -1,13 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[LogLevel](../index.md)/[DEBUG](index.md) - -# DEBUG - -[androidJvm]\ -[DEBUG](index.md) - -## Properties - -| Name | Summary | -|---|---| -| [name](../-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345) | [androidJvm]
val [name](../-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [ordinal](../-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345) | [androidJvm]
val [ordinal](../-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-i-n-f-o/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-i-n-f-o/index.md deleted file mode 100644 index 94a378382aa..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-i-n-f-o/index.md +++ /dev/null @@ -1,13 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[LogLevel](../index.md)/[INFO](index.md) - -# INFO - -[androidJvm]\ -[INFO](index.md) - -## Properties - -| Name | Summary | -|---|---| -| [name](../-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345) | [androidJvm]
val [name](../-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [ordinal](../-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345) | [androidJvm]
val [ordinal](../-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-w-a-r-n-i-n-g/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-w-a-r-n-i-n-g/index.md deleted file mode 100644 index 8bbd47a76c6..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-w-a-r-n-i-n-g/index.md +++ /dev/null @@ -1,13 +0,0 @@ -//[firebase-dataconnect](../../../../index.md)/[com.google.firebase.dataconnect](../../index.md)/[LogLevel](../index.md)/[WARNING](index.md) - -# WARNING - -[androidJvm]\ -[WARNING](index.md) - -## Properties - -| Name | Summary | -|---|---| -| [name](index.md#-372974862%2FProperties%2F1090735345) | [androidJvm]
val [name](index.md#-372974862%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [ordinal](index.md#-739389684%2FProperties%2F1090735345) | [androidJvm]
val [ordinal](index.md#-739389684%2FProperties%2F1090735345): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/entries.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/entries.md deleted file mode 100644 index 9dc69f17c43..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/entries.md +++ /dev/null @@ -1,10 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[LogLevel](index.md)/[entries](entries.md) - -# entries - -[androidJvm]\ -val [entries](entries.md): [EnumEntries](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.enums/-enum-entries/index.html)<[LogLevel](index.md)> - -Returns a representation of an immutable list of all enum entries, in the order they're declared. - -This method may be used to iterate over the enum entries. diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/index.md deleted file mode 100644 index 1cb9167f6f7..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/index.md +++ /dev/null @@ -1,29 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[LogLevel](index.md) - -# LogLevel - -[androidJvm]\ -enum [LogLevel](index.md) : [Enum](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-enum/index.html)<[LogLevel](index.md)> - -## Entries - -| | | -|---|---| -| [DEBUG](-d-e-b-u-g/index.md) | [androidJvm]
[DEBUG](-d-e-b-u-g/index.md) | -| [INFO](-i-n-f-o/index.md) | [androidJvm]
[INFO](-i-n-f-o/index.md) | -| [WARNING](-w-a-r-n-i-n-g/index.md) | [androidJvm]
[WARNING](-w-a-r-n-i-n-g/index.md) | - -## Properties - -| Name | Summary | -|---|---| -| [entries](entries.md) | [androidJvm]
val [entries](entries.md): [EnumEntries](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.enums/-enum-entries/index.html)<[LogLevel](index.md)>
Returns a representation of an immutable list of all enum entries, in the order they're declared. | -| [name](-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345) | [androidJvm]
val [name](-w-a-r-n-i-n-g/index.md#-372974862%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [ordinal](-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345) | [androidJvm]
val [ordinal](-w-a-r-n-i-n-g/index.md#-739389684%2FProperties%2F1090735345): [Int](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/index.html) | - -## Functions - -| Name | Summary | -|---|---| -| [valueOf](value-of.md) | [androidJvm]
fun [valueOf](value-of.md)(value: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [LogLevel](index.md)
Returns the enum constant of this type with the specified name. The string must match exactly an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.) | -| [values](values.md) | [androidJvm]
fun [values](values.md)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[LogLevel](index.md)>
Returns an array containing the constants of this enum type, in the order they're declared. | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/value-of.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/value-of.md deleted file mode 100644 index 6f698edc2ae..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/value-of.md +++ /dev/null @@ -1,14 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[LogLevel](index.md)/[valueOf](value-of.md) - -# valueOf - -[androidJvm]\ -fun [valueOf](value-of.md)(value: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [LogLevel](index.md) - -Returns the enum constant of this type with the specified name. The string must match exactly an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.) - -#### Throws - -| | | -|---|---| -| [IllegalArgumentException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-illegal-argument-exception/index.html) | if this enum type has no constant with the specified name | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/values.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/values.md deleted file mode 100644 index a35fab2c41f..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/values.md +++ /dev/null @@ -1,10 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[LogLevel](index.md)/[values](values.md) - -# values - -[androidJvm]\ -fun [values](values.md)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[LogLevel](index.md)> - -Returns an array containing the constants of this enum type, in the order they're declared. - -This method may be used to iterate over the constants. diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/-mutation-ref.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/-mutation-ref.md deleted file mode 100644 index ab61452398f..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/-mutation-ref.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[MutationRef](index.md)/[MutationRef](-mutation-ref.md) - -# MutationRef - -[androidJvm]\ -constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/execute.md deleted file mode 100644 index c6565aeefed..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/execute.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[MutationRef](index.md)/[execute](execute.md) - -# execute - -[androidJvm]\ -open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/index.md deleted file mode 100644 index 0a5edcdd9b4..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/index.md +++ /dev/null @@ -1,25 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[MutationRef](index.md) - -# MutationRef - -[androidJvm]\ -class [MutationRef](index.md)<[VariablesType](index.md), [ResultType](index.md)>(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) : [BaseRef](../-base-ref/index.md)<[VariablesType](index.md), [ResultType](index.md)> - -## Constructors - -| | | -|---|---| -| [MutationRef](-mutation-ref.md) | [androidJvm]
constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) | - -## Properties - -| Name | Summary | -|---|---| -| [dataConnect](../-base-ref/data-connect.md) | [androidJvm]
val [dataConnect](../-base-ref/data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) | - -## Functions - -| Name | Summary | -|---|---| -| [execute](execute.md) | [androidJvm]
open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) | -| [execute](../../com.google.firebase.dataconnect.generated/execute.md) | [androidJvm]
suspend fun [MutationRef](index.md)<[CreatePostMutation.Variables](../../com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md), [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)>.[execute](../../com.google.firebase.dataconnect.generated/execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), content: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-network-transport-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-network-transport-exception/index.md deleted file mode 100644 index 37692d7299c..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-network-transport-exception/index.md +++ /dev/null @@ -1,26 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[NetworkTransportException](index.md) - -# NetworkTransportException - -[androidJvm]\ -open class [NetworkTransportException](index.md) : [DataConnectException](../-data-connect-exception/index.md) - -## Properties - -| Name | Summary | -|---|---| -| [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | -| [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | - -## Functions - -| Name | Summary | -|---|---| -| [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | -| [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | -| [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | -| [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | -| [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/-query-ref.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/-query-ref.md deleted file mode 100644 index b0f776b2d10..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/-query-ref.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QueryRef](index.md)/[QueryRef](-query-ref.md) - -# QueryRef - -[androidJvm]\ -constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/execute.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/execute.md deleted file mode 100644 index 7df3894ff6d..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/execute.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QueryRef](index.md)/[execute](execute.md) - -# execute - -[androidJvm]\ -open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/index.md deleted file mode 100644 index e4cd167b23c..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/index.md +++ /dev/null @@ -1,27 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QueryRef](index.md) - -# QueryRef - -[androidJvm]\ -class [QueryRef](index.md)<[VariablesType](index.md), [ResultType](index.md)>(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) : [BaseRef](../-base-ref/index.md)<[VariablesType](index.md), [ResultType](index.md)> - -## Constructors - -| | | -|---|---| -| [QueryRef](-query-ref.md) | [androidJvm]
constructor(dataConnect: [FirebaseDataConnect](../-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](../-base-ref/-codec/index.md)<[VariablesType](index.md), [ResultType](index.md)>) | - -## Properties - -| Name | Summary | -|---|---| -| [dataConnect](../-base-ref/data-connect.md) | [androidJvm]
val [dataConnect](../-base-ref/data-connect.md): [FirebaseDataConnect](../-firebase-data-connect/index.md) | - -## Functions - -| Name | Summary | -|---|---| -| [execute](execute.md) | [androidJvm]
open suspend override fun [execute](execute.md)(variables: [VariablesType](index.md)): [ResultType](index.md) | -| [execute](../../com.google.firebase.dataconnect.generated/execute.md) | [androidJvm]
suspend fun [QueryRef](index.md)<[GetPostQuery.Variables](../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md), [GetPostQuery.Result](../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md)>.[execute](../../com.google.firebase.dataconnect.generated/execute.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [GetPostQuery.Result](../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md) | -| [subscribe](subscribe.md) | [androidJvm]
fun [subscribe](subscribe.md)(variables: [VariablesType](index.md)): [QuerySubscription](../-query-subscription/index.md)<[VariablesType](index.md), [ResultType](index.md)> | -| [subscribe](../../com.google.firebase.dataconnect.generated/subscribe.md) | [androidJvm]
fun [QueryRef](index.md)<[GetPostQuery.Variables](../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md), [GetPostQuery.Result](../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md)>.[subscribe](../../com.google.firebase.dataconnect.generated/subscribe.md)(id: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)): [QuerySubscription](../-query-subscription/index.md)<[GetPostQuery.Variables](../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md), [GetPostQuery.Result](../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md)> | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/subscribe.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/subscribe.md deleted file mode 100644 index 7d5f3e9b0a4..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/subscribe.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QueryRef](index.md)/[subscribe](subscribe.md) - -# subscribe - -[androidJvm]\ -fun [subscribe](subscribe.md)(variables: [VariablesType](index.md)): [QuerySubscription](../-query-subscription/index.md)<[VariablesType](index.md), [ResultType](index.md)> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/flow.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/flow.md deleted file mode 100644 index fa96b864074..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/flow.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md)/[flow](flow.md) - -# flow - -[androidJvm]\ -val [flow](flow.md): Flow<[Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>> diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/index.md deleted file mode 100644 index 27cb6cabf6a..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/index.md +++ /dev/null @@ -1,22 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md) - -# QuerySubscription - -[androidJvm]\ -class [QuerySubscription](index.md)<[VariablesType](index.md), [ResultType](index.md)> - -## Properties - -| Name | Summary | -|---|---| -| [flow](flow.md) | [androidJvm]
val [flow](flow.md): Flow<[Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>> | -| [lastResult](last-result.md) | [androidJvm]
val [lastResult](last-result.md): [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>? | -| [variables](variables.md) | [androidJvm]
val [variables](variables.md): [VariablesType](index.md) | - -## Functions - -| Name | Summary | -|---|---| -| [reload](reload.md) | [androidJvm]
fun [reload](reload.md)() | -| [update](update.md) | [androidJvm]
fun [update](update.md)(variables: [VariablesType](index.md)) | -| [update](../../com.google.firebase.dataconnect.generated/update.md) | [androidJvm]
fun [QuerySubscription](index.md)<[GetPostQuery.Variables](../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md), [GetPostQuery.Result](../../com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md)>.[update](../../com.google.firebase.dataconnect.generated/update.md)(block: [GetPostQuery.Variables.Builder](../../com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md).() -> [Unit](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/last-result.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/last-result.md deleted file mode 100644 index 27d2d5cbb41..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/last-result.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md)/[lastResult](last-result.md) - -# lastResult - -[androidJvm]\ -val [lastResult](last-result.md): [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html)<[ResultType](index.md)>? diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/reload.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/reload.md deleted file mode 100644 index 3af7dd6c9fb..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/reload.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md)/[reload](reload.md) - -# reload - -[androidJvm]\ -fun [reload](reload.md)() diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/update.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/update.md deleted file mode 100644 index f6c480fb962..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/update.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md)/[update](update.md) - -# update - -[androidJvm]\ -fun [update](update.md)(variables: [VariablesType](index.md)) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/variables.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/variables.md deleted file mode 100644 index 8265530abee..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/variables.md +++ /dev/null @@ -1,6 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[QuerySubscription](index.md)/[variables](variables.md) - -# variables - -[androidJvm]\ -val [variables](variables.md): [VariablesType](index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-result-decode-exception/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-result-decode-exception/index.md deleted file mode 100644 index 06daec38556..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/-result-decode-exception/index.md +++ /dev/null @@ -1,26 +0,0 @@ -//[firebase-dataconnect](../../../index.md)/[com.google.firebase.dataconnect](../index.md)/[ResultDecodeException](index.md) - -# ResultDecodeException - -[androidJvm]\ -open class [ResultDecodeException](index.md) : [DataConnectException](../-data-connect-exception/index.md) - -## Properties - -| Name | Summary | -|---|---| -| [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345) | [androidJvm]
open val [cause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-654012527%2FProperties%2F1090735345): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)? | -| [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345) | [androidJvm]
open val [message](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1824300659%2FProperties%2F1090735345): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)? | - -## Functions - -| Name | Summary | -|---|---| -| [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345) | [androidJvm]
fun [addSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#282858770%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)) | -| [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345) | [androidJvm]
open fun [fillInStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1102069925%2FFunctions%2F1090735345)(): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345) | [androidJvm]
open fun [getLocalizedMessage](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1043865560%2FFunctions%2F1090735345)(): [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html) | -| [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345) | [androidJvm]
open fun [getStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2050903719%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)> | -| [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345) | [androidJvm]
fun [getSuppressed](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#672492560%2FFunctions%2F1090735345)(): [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)> | -| [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345) | [androidJvm]
open fun [initCause](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-418225042%2FFunctions%2F1090735345)(p0: [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html)): [Throwable](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-throwable/index.html) | -| [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345) | [androidJvm]
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#-1769529168%2FFunctions%2F1090735345)()
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1841853697%2FFunctions%2F1090735345)(p0: [PrintStream](https://developer.android.com/reference/kotlin/java/io/PrintStream.html))
open fun [printStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#1175535278%2FFunctions%2F1090735345)(p0: [PrintWriter](https://developer.android.com/reference/kotlin/java/io/PrintWriter.html)) | -| [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345) | [androidJvm]
open fun [setStackTrace](../../com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md#2135801318%2FFunctions%2F1090735345)(p0: [Array](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html)<[StackTraceElement](https://developer.android.com/reference/kotlin/java/lang/StackTraceElement.html)>) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/index.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/index.md deleted file mode 100644 index b7db742d870..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/index.md +++ /dev/null @@ -1,25 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect](index.md) - -# Package-level declarations - -## Types - -| Name | Summary | -|---|---| -| [BaseRef](-base-ref/index.md) | [androidJvm]
abstract class [BaseRef](-base-ref/index.md)<[VariablesType](-base-ref/index.md), [ResultType](-base-ref/index.md)> | -| [DataConnectException](-data-connect-exception/index.md) | [androidJvm]
open class [DataConnectException](-data-connect-exception/index.md) : [Exception](https://developer.android.com/reference/kotlin/java/lang/Exception.html) | -| [FirebaseDataConnect](-firebase-data-connect/index.md) | [androidJvm]
class [FirebaseDataConnect](-firebase-data-connect/index.md) : [Closeable](https://developer.android.com/reference/kotlin/java/io/Closeable.html) | -| [FirebaseDataConnectSettings](-firebase-data-connect-settings/index.md) | [androidJvm]
class [FirebaseDataConnectSettings](-firebase-data-connect-settings/index.md) | -| [GraphQLException](-graph-q-l-exception/index.md) | [androidJvm]
open class [GraphQLException](-graph-q-l-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | -| [LogLevel](-log-level/index.md) | [androidJvm]
enum [LogLevel](-log-level/index.md) : [Enum](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-enum/index.html)<[LogLevel](-log-level/index.md)> | -| [MutationRef](-mutation-ref/index.md) | [androidJvm]
class [MutationRef](-mutation-ref/index.md)<[VariablesType](-mutation-ref/index.md), [ResultType](-mutation-ref/index.md)>(dataConnect: [FirebaseDataConnect](-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](-base-ref/-codec/index.md)<[VariablesType](-mutation-ref/index.md), [ResultType](-mutation-ref/index.md)>) : [BaseRef](-base-ref/index.md)<[VariablesType](-mutation-ref/index.md), [ResultType](-mutation-ref/index.md)> | -| [NetworkTransportException](-network-transport-exception/index.md) | [androidJvm]
open class [NetworkTransportException](-network-transport-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | -| [QueryRef](-query-ref/index.md) | [androidJvm]
class [QueryRef](-query-ref/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)>(dataConnect: [FirebaseDataConnect](-firebase-data-connect/index.md), operationName: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), operationSet: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), revision: [String](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html), codec: [BaseRef.Codec](-base-ref/-codec/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)>) : [BaseRef](-base-ref/index.md)<[VariablesType](-query-ref/index.md), [ResultType](-query-ref/index.md)> | -| [QuerySubscription](-query-subscription/index.md) | [androidJvm]
class [QuerySubscription](-query-subscription/index.md)<[VariablesType](-query-subscription/index.md), [ResultType](-query-subscription/index.md)> | -| [ResultDecodeException](-result-decode-exception/index.md) | [androidJvm]
open class [ResultDecodeException](-result-decode-exception/index.md) : [DataConnectException](-data-connect-exception/index.md) | - -## Properties - -| Name | Summary | -|---|---| -| [logLevel](log-level.md) | [androidJvm]
@[Volatile](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-volatile/index.html)
var [logLevel](log-level.md): [LogLevel](-log-level/index.md) | diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/log-level.md b/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/log-level.md deleted file mode 100644 index 2eb3bd38cab..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/com.google.firebase.dataconnect/log-level.md +++ /dev/null @@ -1,9 +0,0 @@ -//[firebase-dataconnect](../../index.md)/[com.google.firebase.dataconnect](index.md)/[logLevel](log-level.md) - -# logLevel - -[androidJvm]\ - -@[Volatile](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-volatile/index.html) - -var [logLevel](log-level.md): [LogLevel](-log-level/index.md) diff --git a/firebase-dataconnect/ktdoc/firebase-dataconnect/package-list b/firebase-dataconnect/ktdoc/firebase-dataconnect/package-list deleted file mode 100644 index 2c14b989fa0..00000000000 --- a/firebase-dataconnect/ktdoc/firebase-dataconnect/package-list +++ /dev/null @@ -1,162 +0,0 @@ -$dokka.format:gfm-v1 -$dokka.linkExtension:md -$dokka.location:com.google.firebase.dataconnect.apiproposal////PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal//execute/com.google.firebase.dataconnect.apiproposal.QueryRef[com.google.firebase.dataconnect.apiproposal.GetPostQuery.Variables,com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result]#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/execute.md -$dokka.location:com.google.firebase.dataconnect.apiproposal//getPost/com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect.Queries#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/get-post.md -$dokka.location:com.google.firebase.dataconnect.apiproposal//subscribe/com.google.firebase.dataconnect.apiproposal.QueryRef[com.google.firebase.dataconnect.apiproposal.GetPostQuery.Variables,com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result]#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/subscribe.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef.Codec///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef.Codec/decodeResult/#kotlin.collections.Map[kotlin.String,kotlin.Any?]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/decode-result.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef.Codec/encodeVariables/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/-codec/encode-variables.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef/dataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/data-connect.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/BaseRef/execute/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-base-ref/execute.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/DataConnectException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-data-connect-exception/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/ExecutionException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-execution-exception/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/FirebaseDataConnect.Queries///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/FirebaseDataConnect.Queries/dataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-queries/data-connect.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/FirebaseDataConnect///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/FirebaseDataConnect/FirebaseDataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/-firebase-data-connect.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/FirebaseDataConnect/queries/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-firebase-data-connect/queries.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Companion///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Companion/query/#com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-companion/query.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post.Comment///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post.Comment/Comment/#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/-comment.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post.Comment/content/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/content.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post.Comment/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-comment/id.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post/Post/#kotlin.String#kotlin.collections.List[com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post.Comment]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/-post.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post/comments/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/comments.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result.Post/content/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-post/content.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result/Result/#com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/-result.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Result/post/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-result/post.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Variables///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Variables/Variables/#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/-variables.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery.Variables/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/-variables/id.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuery///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQueryRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-ref/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/GetPostQuerySubscription///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-get-post-query-subscription/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/NetworkTransportException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-network-transport-exception/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/QueryRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/QueryRef/QueryRef/#com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect#kotlin.String#kotlin.String#kotlin.String#com.google.firebase.dataconnect.apiproposal.BaseRef.Codec[TypeParam(bounds=[kotlin.Any?]),TypeParam(bounds=[kotlin.Any?])]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/-query-ref.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/QueryRef/execute/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/execute.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/QueryRef/subscribe/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-ref/subscribe.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/index.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription/flow/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/flow.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription/lastResult/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/last-result.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription/query/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/query.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription/reload/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/reload.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/QuerySubscription/variables/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-query-subscription/variables.md -$dokka.location:com.google.firebase.dataconnect.apiproposal/ResultDecodeException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/-result-decode-exception/index.md -$dokka.location:com.google.firebase.dataconnect.generated////PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md -$dokka.location:com.google.firebase.dataconnect.generated//createPost/com.google.firebase.dataconnect.FirebaseDataConnect.Mutations#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/create-post.md -$dokka.location:com.google.firebase.dataconnect.generated//execute/com.google.firebase.dataconnect.MutationRef[com.google.firebase.dataconnect.generated.CreatePostMutation.Variables,kotlin.Unit]#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md -$dokka.location:com.google.firebase.dataconnect.generated//execute/com.google.firebase.dataconnect.QueryRef[com.google.firebase.dataconnect.generated.GetPostQuery.Variables,com.google.firebase.dataconnect.generated.GetPostQuery.Result]#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/execute.md -$dokka.location:com.google.firebase.dataconnect.generated//getPost/com.google.firebase.dataconnect.FirebaseDataConnect.Queries#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/get-post.md -$dokka.location:com.google.firebase.dataconnect.generated//subscribe/com.google.firebase.dataconnect.QueryRef[com.google.firebase.dataconnect.generated.GetPostQuery.Variables,com.google.firebase.dataconnect.generated.GetPostQuery.Result]#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/subscribe.md -$dokka.location:com.google.firebase.dataconnect.generated//update/com.google.firebase.dataconnect.QuerySubscription[com.google.firebase.dataconnect.generated.GetPostQuery.Variables,com.google.firebase.dataconnect.generated.GetPostQuery.Result]#kotlin.Function1[com.google.firebase.dataconnect.generated.GetPostQuery.Variables.Builder,kotlin.Unit]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/update.md -$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Companion///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/index.md -$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Companion/mutation/#com.google.firebase.dataconnect.FirebaseDataConnect/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-companion/mutation.md -$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables.PostData///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/index.md -$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables.PostData/PostData/#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/-post-data.md -$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables.PostData/content/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/content.md -$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables.PostData/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-post-data/id.md -$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/index.md -$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables/Variables/#com.google.firebase.dataconnect.generated.CreatePostMutation.Variables.PostData/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/-variables.md -$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation.Variables/data/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/-variables/data.md -$dokka.location:com.google.firebase.dataconnect.generated/CreatePostMutation///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-create-post-mutation/index.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Companion///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/index.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Companion/query/#com.google.firebase.dataconnect.FirebaseDataConnect/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-companion/query.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post.Comment///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/index.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post.Comment/Comment/#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/-comment.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post.Comment/content/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/content.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post.Comment/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-comment/id.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/index.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post/Post/#kotlin.String#kotlin.collections.List[com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post.Comment]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/-post.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post/comments/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/comments.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result.Post/content/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-post/content.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/index.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result/Result/#com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/-result.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Result/post/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-result/post.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables.Builder///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/index.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables.Builder/Builder/#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/-builder.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables.Builder/build/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/build.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables.Builder/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-builder/id.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/index.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables/Variables/#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/-variables.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables/build/#kotlin.Function1[com.google.firebase.dataconnect.generated.GetPostQuery.Variables.Builder,kotlin.Unit]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/build.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables/builder/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/builder.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery.Variables/id/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/-variables/id.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuery///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query/index.md -$dokka.location:com.google.firebase.dataconnect.generated/GetPostQuerySubscription///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect.generated/-get-post-query-subscription/index.md -$dokka.location:com.google.firebase.dataconnect////PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/index.md -$dokka.location:com.google.firebase.dataconnect//logLevel/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/log-level.md -$dokka.location:com.google.firebase.dataconnect/BaseRef.Codec///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/index.md -$dokka.location:com.google.firebase.dataconnect/BaseRef.Codec/decodeResult/#kotlin.collections.Map[kotlin.String,kotlin.Any?]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/decode-result.md -$dokka.location:com.google.firebase.dataconnect/BaseRef.Codec/encodeVariables/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/-codec/encode-variables.md -$dokka.location:com.google.firebase.dataconnect/BaseRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/index.md -$dokka.location:com.google.firebase.dataconnect/BaseRef/dataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/data-connect.md -$dokka.location:com.google.firebase.dataconnect/BaseRef/execute/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-base-ref/execute.md -$dokka.location:com.google.firebase.dataconnect/DataConnectException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-data-connect-exception/index.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Companion///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/index.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Companion/getInstance/#com.google.firebase.FirebaseApp#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Companion/getInstance/#kotlin.String#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-companion/get-instance.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Mutations///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/index.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Mutations/dataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-mutations/data-connect.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Queries///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/index.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect.Queries/dataConnect/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/-queries/data-connect.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/index.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/app/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/app.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/close/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/close.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/location/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/location.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/mutations/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/mutations.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/queries/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/queries.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/service/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/service.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/settings/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/settings.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/toString/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/to-string.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnect/updateSettings/#kotlin.Function1[com.google.firebase.dataconnect.FirebaseDataConnectSettings.Builder,kotlin.Unit]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect/update-settings.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/index.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/build/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/build/#kotlin.Function1[com.google.firebase.dataconnect.FirebaseDataConnectSettings.Builder,kotlin.Unit]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/build.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/connectToEmulator/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/connect-to-emulator.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/hostName/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/host-name.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/port/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/port.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Builder/sslEnabled/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-builder/ssl-enabled.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Companion///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/index.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings.Companion/defaults/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/-companion/defaults.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/index.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/builder/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/builder.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/equals/#kotlin.Any?/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/equals.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/hashCode/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/hash-code.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/hostName/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/host-name.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/port/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/port.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/sslEnabled/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/ssl-enabled.md -$dokka.location:com.google.firebase.dataconnect/FirebaseDataConnectSettings/toString/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-firebase-data-connect-settings/to-string.md -$dokka.location:com.google.firebase.dataconnect/GraphQLException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/index.md -$dokka.location:com.google.firebase.dataconnect/GraphQLException/errors/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-graph-q-l-exception/errors.md -$dokka.location:com.google.firebase.dataconnect/LogLevel.DEBUG///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-d-e-b-u-g/index.md -$dokka.location:com.google.firebase.dataconnect/LogLevel.INFO///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-i-n-f-o/index.md -$dokka.location:com.google.firebase.dataconnect/LogLevel.WARNING///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}firebase-dataconnect/com.google.firebase.dataconnect/-log-level/-w-a-r-n-i-n-g/index.md -$dokka.location:com.google.firebase.dataconnect/LogLevel///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/index.md -$dokka.location:com.google.firebase.dataconnect/LogLevel/entries/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/entries.md -$dokka.location:com.google.firebase.dataconnect/LogLevel/valueOf/#kotlin.String/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/value-of.md -$dokka.location:com.google.firebase.dataconnect/LogLevel/values/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-log-level/values.md -$dokka.location:com.google.firebase.dataconnect/MutationRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/index.md -$dokka.location:com.google.firebase.dataconnect/MutationRef/MutationRef/#com.google.firebase.dataconnect.FirebaseDataConnect#kotlin.String#kotlin.String#kotlin.String#com.google.firebase.dataconnect.BaseRef.Codec[TypeParam(bounds=[kotlin.Any?]),TypeParam(bounds=[kotlin.Any?])]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/-mutation-ref.md -$dokka.location:com.google.firebase.dataconnect/MutationRef/execute/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-mutation-ref/execute.md -$dokka.location:com.google.firebase.dataconnect/NetworkTransportException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-network-transport-exception/index.md -$dokka.location:com.google.firebase.dataconnect/QueryRef///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/index.md -$dokka.location:com.google.firebase.dataconnect/QueryRef/QueryRef/#com.google.firebase.dataconnect.FirebaseDataConnect#kotlin.String#kotlin.String#kotlin.String#com.google.firebase.dataconnect.BaseRef.Codec[TypeParam(bounds=[kotlin.Any?]),TypeParam(bounds=[kotlin.Any?])]/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/-query-ref.md -$dokka.location:com.google.firebase.dataconnect/QueryRef/execute/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/execute.md -$dokka.location:com.google.firebase.dataconnect/QueryRef/subscribe/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-ref/subscribe.md -$dokka.location:com.google.firebase.dataconnect/QuerySubscription///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/index.md -$dokka.location:com.google.firebase.dataconnect/QuerySubscription/flow/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/flow.md -$dokka.location:com.google.firebase.dataconnect/QuerySubscription/lastResult/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/last-result.md -$dokka.location:com.google.firebase.dataconnect/QuerySubscription/reload/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/reload.md -$dokka.location:com.google.firebase.dataconnect/QuerySubscription/update/#TypeParam(bounds=[kotlin.Any?])/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/update.md -$dokka.location:com.google.firebase.dataconnect/QuerySubscription/variables/#/PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-query-subscription/variables.md -$dokka.location:com.google.firebase.dataconnect/ResultDecodeException///PointingToDeclaration/firebase-dataconnect/com.google.firebase.dataconnect/-result-decode-exception/index.md -com.google.firebase.dataconnect -com.google.firebase.dataconnect.apiproposal -com.google.firebase.dataconnect.generated - diff --git a/firebase-dataconnect/ktdoc/index.md b/firebase-dataconnect/ktdoc/index.md deleted file mode 100644 index fd7e2dbff73..00000000000 --- a/firebase-dataconnect/ktdoc/index.md +++ /dev/null @@ -1,11 +0,0 @@ -//[firebase-dataconnect](index.md) - -# firebase-dataconnect - -## Packages - -| Name | -|---| -| [com.google.firebase.dataconnect](firebase-dataconnect/com.google.firebase.dataconnect/index.md) | -| [com.google.firebase.dataconnect.apiproposal](firebase-dataconnect/com.google.firebase.dataconnect.apiproposal/index.md) | -| [com.google.firebase.dataconnect.generated](firebase-dataconnect/com.google.firebase.dataconnect.generated/index.md) | From b729961f4c72155bdc3583829ad72a3fd9d3937c Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Nov 2023 16:07:55 -0500 Subject: [PATCH 051/573] Make FirebaseDataConnect.settings immutable, and set on creation --- .../dataconnect/FirebaseDataConnectTest.kt | 53 ++++++ .../testutil/TestDataConnectFactory.kt | 18 +- .../dataconnect/FirebaseDataConnect.kt | 165 ++++++++++-------- .../dataconnect/FirebaseDataConnectFactory.kt | 16 +- .../FirebaseDataConnectSettings.kt | 2 + 5 files changed, 169 insertions(+), 85 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 69b549a6635..4c450734f01 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -32,6 +32,7 @@ import kotlin.concurrent.thread import kotlin.concurrent.withLock import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -122,6 +123,58 @@ class FirebaseDataConnectTest { assertThat(instance2A).isNotSameInstanceAs(instance1B) } + @Test + fun getInstance_should_return_the_cached_instance_if_settings_compare_equal() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val settings = FirebaseDataConnectSettings.defaults.build { hostName = "TestHostName" } + val instance1 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService", settings) + val instance2 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService", settings) + assertThat(instance1).isSameInstanceAs(instance2) + } + + @Test + fun getInstance_should_return_the_cached_instance_if_settings_are_null() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val settings = FirebaseDataConnectSettings.defaults.build { hostName = "TestHostName" } + val instance1 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService", settings) + val instance2 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService", null) + assertThat(instance1).isSameInstanceAs(instance2) + } + + @Test + fun getInstance_should_throw_if_settings_compare_unequal_to_settings_of_cached_instance() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val settings1 = FirebaseDataConnectSettings.defaults.build { hostName = "HostName1" } + val settings2 = FirebaseDataConnectSettings.defaults.build { hostName = "HostName2" } + val instance1 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1", settings1) + + assertThrows(IllegalArgumentException::class.java) { + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1", settings2) + } + + val instance2 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1", settings1) + assertThat(instance1).isSameInstanceAs(instance2) + } + + @Test + fun getInstance_should_return_new_instance_if_settings_and_app_are_both_different() { + val nonDefaultApp1 = firebaseAppFactory.newInstance() + val nonDefaultApp2 = firebaseAppFactory.newInstance() + val settings1 = FirebaseDataConnectSettings.defaults.build { hostName = "HostName1" } + val settings2 = FirebaseDataConnectSettings.defaults.build { hostName = "HostName2" } + val instance1 = + FirebaseDataConnect.getInstance(nonDefaultApp1, "TestLocation", "TestService", settings1) + val instance2 = + FirebaseDataConnect.getInstance(nonDefaultApp2, "TestLocation", "TestService", settings2) + assertThat(instance1).isNotSameInstanceAs(instance2) + } + @Test fun getInstance_should_be_thread_safe() { val apps = diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt index ad25ba258ed..ac83ac8df5d 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt @@ -26,17 +26,13 @@ class TestDataConnectFactory : fun newInstance(location: String? = null, service: String? = null): FirebaseDataConnect = newInstance(Params(location = location, service = service)) - override fun createInstance(instanceId: String, params: Params?): FirebaseDataConnect { - val instance = - FirebaseDataConnect.getInstance( - location = params?.location ?: "TestLocation$instanceId", - service = params?.service ?: "TestService$instanceId" - ) - - instance.updateSettings { connectToEmulator() } - - return instance - } + override fun createInstance(instanceId: String, params: Params?) = + FirebaseDataConnect.getInstance( + location = params?.location ?: "TestLocation$instanceId", + service = params?.service ?: "TestService$instanceId" + ) { + connectToEmulator() + } override fun destroyInstance(instance: FirebaseDataConnect) { instance.close() diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 669697580d8..803d7e50ea2 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -17,10 +17,9 @@ import android.content.Context import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app +import com.google.firebase.concurrent.FirebaseExecutors import java.io.Closeable import java.util.concurrent.Executor -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.* import kotlinx.coroutines.* class FirebaseDataConnect @@ -32,7 +31,8 @@ internal constructor( val service: String, internal val blockingExecutor: Executor, internal val nonBlockingExecutor: Executor, - private val creator: FirebaseDataConnectFactory + private val creator: FirebaseDataConnectFactory, + val settings: FirebaseDataConnectSettings, ) : Closeable { private val logger = @@ -56,75 +56,58 @@ internal constructor( CoroutineName("FirebaseDataConnect") ) - private val lock = ReentrantReadWriteLock() - private var settingsFrozen = false - private var closed = false + // Dispatcher used to access `this.closed` and `this.grpcClient` simple. + private val sequentialDispatcher = + FirebaseExecutors.newSequentialExecutor(nonBlockingExecutor).asCoroutineDispatcher() - var settings: FirebaseDataConnectSettings = FirebaseDataConnectSettings.defaults - get() { - lock.read { - return field - } - } - set(value) { - lock.write { - if (closed) { - throw IllegalStateException("instance has been closed") - } - if (settingsFrozen) { - throw IllegalStateException("settings cannot be modified after they are used") - } - field = value - } - logger.debug { "Settings changed to $value" } - } - - fun updateSettings(block: FirebaseDataConnectSettings.Builder.() -> Unit) { - settings = settings.builder.build(block) - } + // This boolean value MUST only be accessed from code running on `sequentialDispatcher`. + private var closed = false + // This reference MUST only be set or dereferenced from code running on `sequentialDispatcher`. private val grpcClient: DataConnectGrpcClient by lazy { logger.debug { "DataConnectGrpcClient initialization started" } - lock.write { - if (closed) { - throw IllegalStateException("instance has been closed") - } - settingsFrozen = true - - DataConnectGrpcClient( - context = context, - projectId = projectId, - location = location, - service = service, - hostName = settings.hostName, - port = settings.port, - sslEnabled = settings.sslEnabled, - executor = blockingExecutor, - creatorLoggerId = logger.id, - ) - .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } + if (closed) { + throw IllegalStateException("instance has been closed") } + DataConnectGrpcClient( + context = context, + projectId = projectId, + location = location, + service = service, + hostName = settings.hostName, + port = settings.port, + sslEnabled = settings.sslEnabled, + executor = blockingExecutor, + creatorLoggerId = logger.id, + ) + .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } } internal suspend fun executeQuery(ref: QueryRef, variables: V): R = - ref.codec.decodeResult( - grpcClient.executeQuery( - operationName = ref.operationName, - operationSet = ref.operationSet, - revision = ref.revision, - variables = ref.codec.encodeVariables(variables) - ) - ) + withContext(sequentialDispatcher) { grpcClient } + .run { + ref.codec.decodeResult( + executeQuery( + operationName = ref.operationName, + operationSet = ref.operationSet, + revision = ref.revision, + variables = ref.codec.encodeVariables(variables) + ) + ) + } internal suspend fun executeMutation(ref: MutationRef, variables: V): R = - ref.codec.decodeResult( - grpcClient.executeMutation( - operationName = ref.operationName, - operationSet = ref.operationSet, - revision = ref.revision, - variables = ref.codec.encodeVariables(variables) - ) - ) + withContext(sequentialDispatcher) { grpcClient } + .run { + ref.codec.decodeResult( + executeMutation( + operationName = ref.operationName, + operationSet = ref.operationSet, + revision = ref.revision, + variables = ref.codec.encodeVariables(variables) + ) + ) + } fun query( operationName: String, @@ -156,28 +139,66 @@ internal constructor( override fun close() { logger.debug { "close() called" } - lock.write { - coroutineScope.cancel() - try { - grpcClient.close() - } finally { + runBlocking(sequentialDispatcher) { + if (!closed) { + doClose() closed = true - creator.remove(this) } } } + private fun doClose() { + grpcClient.close() + coroutineScope.cancel() + creator.remove(this@FirebaseDataConnect) + } + override fun toString(): String { return "FirebaseDataConnect" + "{app=${app.name}, projectId=$projectId, location=$location, service=$service}" } companion object { - fun getInstance(location: String, service: String): FirebaseDataConnect = - getInstance(Firebase.app, location, service) + fun getInstance( + app: FirebaseApp, + location: String, + service: String, + settings: FirebaseDataConnectSettings? = null + ): FirebaseDataConnect = + app.get(FirebaseDataConnectFactory::class.java).run { + get(location = location, service = service, settings = settings) + } - fun getInstance(app: FirebaseApp, location: String, service: String): FirebaseDataConnect = - app.get(FirebaseDataConnectFactory::class.java).run { get(location, service) } + fun getInstance( + location: String, + service: String, + settings: FirebaseDataConnectSettings? = null + ): FirebaseDataConnect = + getInstance(app = Firebase.app, location = location, service = service, settings = settings) + + fun getInstance( + app: FirebaseApp, + location: String, + service: String, + settingsBlock: FirebaseDataConnectSettings.Builder.() -> Unit + ): FirebaseDataConnect = + getInstance( + app = app, + location = location, + service = service, + settings = FirebaseDataConnectSettings.defaults.build(settingsBlock) + ) + + fun getInstance( + location: String, + service: String, + settingsBlock: FirebaseDataConnectSettings.Builder.() -> Unit + ): FirebaseDataConnect = + getInstance( + location = location, + service = service, + settings = FirebaseDataConnectSettings.defaults.build(settingsBlock) + ) } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt index a2a2be88611..06bf4073687 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt @@ -41,7 +41,11 @@ internal class FirebaseDataConnectFactory( private val instancesByCacheKey = mutableMapOf() private var closed = false - fun get(location: String, service: String): FirebaseDataConnect { + fun get( + location: String, + service: String, + settings: FirebaseDataConnectSettings? + ): FirebaseDataConnect { val key = InstanceCacheKey(location = location, service = service) lock.withLock { if (closed) { @@ -49,6 +53,13 @@ internal class FirebaseDataConnectFactory( } val cachedInstance = instancesByCacheKey[key] if (cachedInstance !== null) { + if (settings !== null && settings != cachedInstance.settings) { + throw IllegalArgumentException( + "The cached FirebaseDataConnect instance ($cachedInstance)" + + " must have the same settings as the specified settings; however, they are different" + + " (cached settings: ${cachedInstance.settings}, specified settings: $settings)" + ) + } return cachedInstance } @@ -62,7 +73,8 @@ internal class FirebaseDataConnectFactory( service = service, blockingExecutor = blockingExecutor, nonBlockingExecutor = nonBlockingExecutor, - creator = this + creator = this, + settings = settings ?: FirebaseDataConnectSettings.defaults ) instancesByCacheKey[key] = newInstance return newInstance diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt index 15700dff5e4..e4cf99d84b7 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt @@ -27,6 +27,8 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin val builder: Builder get() = Builder(this) + fun build(block: Builder.() -> Unit): FirebaseDataConnectSettings = builder.build(block) + companion object { val defaults get() = From 2fff1d44b7aef86a517625ea6c0d33eac5fc6a95 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Nov 2023 16:09:37 -0500 Subject: [PATCH 052/573] api.txt generated by running ../gradlew generateApiTxtFile --- firebase-dataconnect/api.txt | 359 ++++++++++++++++++++++++++++++++++- 1 file changed, 358 insertions(+), 1 deletion(-) diff --git a/firebase-dataconnect/api.txt b/firebase-dataconnect/api.txt index c9f0db4470f..ca753efa9a5 100644 --- a/firebase-dataconnect/api.txt +++ b/firebase-dataconnect/api.txt @@ -1,7 +1,364 @@ // Signature format: 2.0 package com.google.firebase.dataconnect { - public class FirebaseDataConnect { + public abstract class BaseRef { + method @Nullable public abstract suspend Object execute(@Nullable VariablesType variables, @NonNull kotlin.coroutines.Continuation); + method @NonNull public final com.google.firebase.dataconnect.FirebaseDataConnect getDataConnect(); + property @NonNull public final com.google.firebase.dataconnect.FirebaseDataConnect dataConnect; + } + + public static interface BaseRef.Codec { + method public ResultType decodeResult(@NonNull java.util.Map map); + method @NonNull public java.util.Map encodeVariables(@Nullable VariablesType variables); + } + + public class DataConnectException extends java.lang.Exception { + } + + public final class DataConnectGrpcClientKt { + } + + public final class FirebaseDataConnect implements java.io.Closeable { + method public void close(); + method @NonNull public com.google.firebase.FirebaseApp getApp(); + method @NonNull public String getLocation(); + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnect.Mutations getMutations(); + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnect.Queries getQueries(); + method @NonNull public String getService(); + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnectSettings getSettings(); + method @NonNull public com.google.firebase.dataconnect.MutationRef mutation(@NonNull String operationName, @NonNull String operationSet, @NonNull String revision, @NonNull com.google.firebase.dataconnect.BaseRef.Codec codec); + method @NonNull public com.google.firebase.dataconnect.QueryRef query(@NonNull String operationName, @NonNull String operationSet, @NonNull String revision, @NonNull com.google.firebase.dataconnect.BaseRef.Codec codec); + property @NonNull public final com.google.firebase.FirebaseApp app; + property @NonNull public final String location; + property @NonNull public final com.google.firebase.dataconnect.FirebaseDataConnect.Mutations mutations; + property @NonNull public final com.google.firebase.dataconnect.FirebaseDataConnect.Queries queries; + property @NonNull public final String service; + property @NonNull public final com.google.firebase.dataconnect.FirebaseDataConnectSettings settings; + field @NonNull public static final com.google.firebase.dataconnect.FirebaseDataConnect.Companion Companion; + } + + public static final class FirebaseDataConnect.Companion { + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnect getInstance(@NonNull com.google.firebase.FirebaseApp app, @NonNull String location, @NonNull String service, @Nullable com.google.firebase.dataconnect.FirebaseDataConnectSettings settings = null); + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnect getInstance(@NonNull String location, @NonNull String service, @Nullable com.google.firebase.dataconnect.FirebaseDataConnectSettings settings = null); + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnect getInstance(@NonNull com.google.firebase.FirebaseApp app, @NonNull String location, @NonNull String service, @NonNull kotlin.jvm.functions.Function1 settingsBlock); + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnect getInstance(@NonNull String location, @NonNull String service, @NonNull kotlin.jvm.functions.Function1 settingsBlock); + } + + public static final class FirebaseDataConnect.Mutations { + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnect getDataConnect(); + property @NonNull public final com.google.firebase.dataconnect.FirebaseDataConnect dataConnect; + } + + public static final class FirebaseDataConnect.Queries { + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnect getDataConnect(); + property @NonNull public final com.google.firebase.dataconnect.FirebaseDataConnect dataConnect; + } + + public final class FirebaseDataConnectSettings { + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnectSettings build(@NonNull kotlin.jvm.functions.Function1 block); + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnectSettings.Builder getBuilder(); + method @NonNull public String getHostName(); + method public int getPort(); + method public boolean getSslEnabled(); + property @NonNull public final com.google.firebase.dataconnect.FirebaseDataConnectSettings.Builder builder; + property @NonNull public final String hostName; + property public final int port; + property public final boolean sslEnabled; + field @NonNull public static final com.google.firebase.dataconnect.FirebaseDataConnectSettings.Companion Companion; + } + + public static final class FirebaseDataConnectSettings.Builder { + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnectSettings build(); + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnectSettings build(@NonNull kotlin.jvm.functions.Function1 block); + method public void connectToEmulator(); + method @NonNull public String getHostName(); + method public int getPort(); + method public boolean getSslEnabled(); + method public void setHostName(@NonNull String); + method public void setPort(int); + method public void setSslEnabled(boolean); + property @NonNull public final String hostName; + property public final int port; + property public final boolean sslEnabled; + } + + public static final class FirebaseDataConnectSettings.Companion { + method @NonNull public com.google.firebase.dataconnect.FirebaseDataConnectSettings getDefaults(); + property @NonNull public final com.google.firebase.dataconnect.FirebaseDataConnectSettings defaults; + } + + public class GraphQLException extends com.google.firebase.dataconnect.DataConnectException { + method @NonNull public final java.util.List getErrors(); + property @NonNull public final java.util.List errors; + } + + public enum LogLevel { + method @NonNull public static com.google.firebase.dataconnect.LogLevel valueOf(@NonNull String name) throws java.lang.IllegalArgumentException; + method @NonNull public static com.google.firebase.dataconnect.LogLevel[] values(); + enum_constant public static final com.google.firebase.dataconnect.LogLevel DEBUG; + enum_constant public static final com.google.firebase.dataconnect.LogLevel INFO; + enum_constant public static final com.google.firebase.dataconnect.LogLevel WARNING; + } + + public final class LoggerKt { + method @NonNull public static com.google.firebase.dataconnect.LogLevel getLogLevel(); + method public static void setLogLevel(@NonNull com.google.firebase.dataconnect.LogLevel); + property @NonNull public static final com.google.firebase.dataconnect.LogLevel logLevel; + } + + public final class MutationRef extends com.google.firebase.dataconnect.BaseRef { + method @Nullable public suspend Object execute(@Nullable VariablesType variables, @NonNull kotlin.coroutines.Continuation); + } + + public class NetworkTransportException extends com.google.firebase.dataconnect.DataConnectException { + } + + public final class QueryRef extends com.google.firebase.dataconnect.BaseRef { + method @Nullable public suspend Object execute(@Nullable VariablesType variables, @NonNull kotlin.coroutines.Continuation); + method @NonNull public com.google.firebase.dataconnect.QuerySubscription subscribe(@Nullable VariablesType variables); + } + + public final class QuerySubscription { + method @NonNull public kotlinx.coroutines.flow.Flow> getFlow(); + method @Nullable public com.google.firebase.dataconnect.QuerySubscription.Message getLastResult(); + method public VariablesType getVariables(); + method public void reload(); + method public void update(@Nullable VariablesType variables); + property @NonNull public final kotlinx.coroutines.flow.Flow> flow; + property @Nullable public final com.google.firebase.dataconnect.QuerySubscription.Message lastResult; + property public final VariablesType variables; + } + + public static final class QuerySubscription.Message { + ctor public QuerySubscription.Message(@Nullable VariablesType variables, @NonNull Object result); + method @NonNull public Object getResult(); + method public VariablesType getVariables(); + property @NonNull public final Object result; + property public final VariablesType variables; + } + + public final class QuerySubscriptionKt { + } + + public class ResultDecodeException extends com.google.firebase.dataconnect.DataConnectException { + } + +} + +package com.google.firebase.dataconnect.apiproposal { + + public abstract class BaseRef { + method @Nullable public abstract suspend Object execute(@Nullable VariablesType variables, @NonNull kotlin.coroutines.Continuation); + method @NonNull public final com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect getDataConnect(); + property @NonNull public final com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect dataConnect; + } + + public static interface BaseRef.Codec { + method public ResultType decodeResult(@NonNull java.util.Map map); + method @NonNull public java.util.Map encodeVariables(@Nullable VariablesType variables); + } + + public class DataConnectException extends java.lang.Exception { + } + + public final class FirebaseDataConnect { + ctor public FirebaseDataConnect(); + method @NonNull public com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect.Queries getQueries(); + method @NonNull public com.google.firebase.dataconnect.apiproposal.QueryRef query(@NonNull String operationName, @NonNull String operationSet, @NonNull String revision, @NonNull com.google.firebase.dataconnect.apiproposal.BaseRef.Codec codec); + property @NonNull public final com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect.Queries queries; + } + + public static final class FirebaseDataConnect.Queries { + method @NonNull public com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect getDataConnect(); + property @NonNull public final com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect dataConnect; + } + + public final class GetPostQuery { + field @NonNull public static final com.google.firebase.dataconnect.apiproposal.GetPostQuery.Companion Companion; + } + + public static final class GetPostQuery.Companion { + method @NonNull public com.google.firebase.dataconnect.apiproposal.QueryRef query(@NonNull com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect dataConnect); + } + + public static final class GetPostQuery.Result { + ctor public GetPostQuery.Result(@NonNull com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post post); + method @NonNull public com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post component1(); + method @NonNull public com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result copy(@NonNull com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post post); + method @NonNull public com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post getPost(); + property @NonNull public final com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post post; + } + + public static final class GetPostQuery.Result.Post { + ctor public GetPostQuery.Result.Post(@NonNull String content, @NonNull java.util.List comments); + method @NonNull public String component1(); + method @NonNull public java.util.List component2(); + method @NonNull public com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post copy(@NonNull String content, @NonNull java.util.List comments); + method @NonNull public java.util.List getComments(); + method @NonNull public String getContent(); + property @NonNull public final java.util.List comments; + property @NonNull public final String content; + } + + public static final class GetPostQuery.Result.Post.Comment { + ctor public GetPostQuery.Result.Post.Comment(@NonNull String id, @NonNull String content); + method @NonNull public String component1(); + method @NonNull public String component2(); + method @NonNull public com.google.firebase.dataconnect.apiproposal.GetPostQuery.Result.Post.Comment copy(@NonNull String id, @NonNull String content); + method @NonNull public String getContent(); + method @NonNull public String getId(); + property @NonNull public final String content; + property @NonNull public final String id; + } + + public static final class GetPostQuery.Variables { + ctor public GetPostQuery.Variables(@NonNull String id); + method @NonNull public String component1(); + method @NonNull public com.google.firebase.dataconnect.apiproposal.GetPostQuery.Variables copy(@NonNull String id); + method @NonNull public String getId(); + property @NonNull public final String id; + } + + public class GraphQLException extends com.google.firebase.dataconnect.apiproposal.DataConnectException { + method @NonNull public final java.util.List getErrors(); + property @NonNull public final java.util.List errors; + } + + public class NetworkTransportException extends com.google.firebase.dataconnect.apiproposal.DataConnectException { + } + + public final class QueryApiProposalKt { + method @Nullable public static suspend Object execute(@NonNull com.google.firebase.dataconnect.apiproposal.QueryRef, @NonNull String id, @NonNull kotlin.coroutines.Continuation); + method @NonNull public static com.google.firebase.dataconnect.apiproposal.QueryRef getGetPost(@NonNull com.google.firebase.dataconnect.apiproposal.FirebaseDataConnect.Queries); + method @NonNull public static com.google.firebase.dataconnect.apiproposal.QuerySubscription subscribe(@NonNull com.google.firebase.dataconnect.apiproposal.QueryRef, @NonNull String id); + } + + public final class QueryRef extends com.google.firebase.dataconnect.apiproposal.BaseRef { + method @Nullable public suspend Object execute(@Nullable VariablesType variables, @NonNull kotlin.coroutines.Continuation); + method @NonNull public com.google.firebase.dataconnect.apiproposal.QuerySubscription subscribe(@Nullable VariablesType variables); + } + + public final class QuerySubscription { + method @NonNull public kotlinx.coroutines.flow.Flow> getFlow(); + method @Nullable public com.google.firebase.dataconnect.apiproposal.QuerySubscription.Message getLastResult(); + method @NonNull public com.google.firebase.dataconnect.apiproposal.QueryRef getQuery(); + method public VariablesType getVariables(); + method public void reload(); + property @NonNull public final kotlinx.coroutines.flow.Flow> flow; + property @Nullable public final com.google.firebase.dataconnect.apiproposal.QuerySubscription.Message lastResult; + property @NonNull public final com.google.firebase.dataconnect.apiproposal.QueryRef query; + property public final VariablesType variables; + } + + public static final class QuerySubscription.Message { + ctor public QuerySubscription.Message(@Nullable VariablesType variables, @NonNull Object result); + method @NonNull public Object getResult(); + method public VariablesType getVariables(); + property @NonNull public final Object result; + property public final VariablesType variables; + } + + public class ResultDecodeException extends com.google.firebase.dataconnect.apiproposal.DataConnectException { + } + +} + +package com.google.firebase.dataconnect.generated { + + public final class CreatePostMutation { + field @NonNull public static final com.google.firebase.dataconnect.generated.CreatePostMutation.Companion Companion; + } + + public static final class CreatePostMutation.Companion { + method @NonNull public com.google.firebase.dataconnect.MutationRef mutation(@NonNull com.google.firebase.dataconnect.FirebaseDataConnect dataConnect); + } + + public static final class CreatePostMutation.Variables { + ctor public CreatePostMutation.Variables(@NonNull com.google.firebase.dataconnect.generated.CreatePostMutation.Variables.PostData data); + method @NonNull public com.google.firebase.dataconnect.generated.CreatePostMutation.Variables.PostData component1(); + method @NonNull public com.google.firebase.dataconnect.generated.CreatePostMutation.Variables copy(@NonNull com.google.firebase.dataconnect.generated.CreatePostMutation.Variables.PostData data); + method @NonNull public com.google.firebase.dataconnect.generated.CreatePostMutation.Variables.PostData getData(); + property @NonNull public final com.google.firebase.dataconnect.generated.CreatePostMutation.Variables.PostData data; + } + + public static final class CreatePostMutation.Variables.PostData { + ctor public CreatePostMutation.Variables.PostData(@NonNull String id, @NonNull String content); + method @NonNull public String component1(); + method @NonNull public String component2(); + method @NonNull public com.google.firebase.dataconnect.generated.CreatePostMutation.Variables.PostData copy(@NonNull String id, @NonNull String content); + method @NonNull public String getContent(); + method @NonNull public String getId(); + property @NonNull public final String content; + property @NonNull public final String id; + } + + public final class CreatePostMutationKt { + method @Nullable public static suspend Object execute(@NonNull com.google.firebase.dataconnect.MutationRef, @NonNull String id, @NonNull String content, @NonNull kotlin.coroutines.Continuation); + method @NonNull public static com.google.firebase.dataconnect.MutationRef getCreatePost(@NonNull com.google.firebase.dataconnect.FirebaseDataConnect.Mutations); + } + + public final class GetPostQuery { + field @NonNull public static final com.google.firebase.dataconnect.generated.GetPostQuery.Companion Companion; + } + + public static final class GetPostQuery.Companion { + method @NonNull public com.google.firebase.dataconnect.QueryRef query(@NonNull com.google.firebase.dataconnect.FirebaseDataConnect dataConnect); + } + + public static final class GetPostQuery.Result { + ctor public GetPostQuery.Result(@NonNull com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post post); + method @NonNull public com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post component1(); + method @NonNull public com.google.firebase.dataconnect.generated.GetPostQuery.Result copy(@NonNull com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post post); + method @NonNull public com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post getPost(); + property @NonNull public final com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post post; + } + + public static final class GetPostQuery.Result.Post { + ctor public GetPostQuery.Result.Post(@NonNull String content, @NonNull java.util.List comments); + method @NonNull public String component1(); + method @NonNull public java.util.List component2(); + method @NonNull public com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post copy(@NonNull String content, @NonNull java.util.List comments); + method @NonNull public java.util.List getComments(); + method @NonNull public String getContent(); + property @NonNull public final java.util.List comments; + property @NonNull public final String content; + } + + public static final class GetPostQuery.Result.Post.Comment { + ctor public GetPostQuery.Result.Post.Comment(@NonNull String id, @NonNull String content); + method @NonNull public String component1(); + method @NonNull public String component2(); + method @NonNull public com.google.firebase.dataconnect.generated.GetPostQuery.Result.Post.Comment copy(@NonNull String id, @NonNull String content); + method @NonNull public String getContent(); + method @NonNull public String getId(); + property @NonNull public final String content; + property @NonNull public final String id; + } + + public static final class GetPostQuery.Variables { + ctor public GetPostQuery.Variables(@NonNull String id); + method @NonNull public com.google.firebase.dataconnect.generated.GetPostQuery.Variables build(@NonNull kotlin.jvm.functions.Function1 block); + method @NonNull public String component1(); + method @NonNull public com.google.firebase.dataconnect.generated.GetPostQuery.Variables copy(@NonNull String id); + method @NonNull public com.google.firebase.dataconnect.generated.GetPostQuery.Variables.Builder getBuilder(); + method @NonNull public String getId(); + property @NonNull public final com.google.firebase.dataconnect.generated.GetPostQuery.Variables.Builder builder; + property @NonNull public final String id; + } + + public static final class GetPostQuery.Variables.Builder { + ctor public GetPostQuery.Variables.Builder(@NonNull String id); + method @NonNull public com.google.firebase.dataconnect.generated.GetPostQuery.Variables build(); + method @NonNull public String getId(); + method public void setId(@NonNull String); + property @NonNull public final String id; + } + + public final class GetPostQueryKt { + method @Nullable public static suspend Object execute(@NonNull com.google.firebase.dataconnect.QueryRef, @NonNull String id, @NonNull kotlin.coroutines.Continuation); + method @NonNull public static com.google.firebase.dataconnect.QueryRef getGetPost(@NonNull com.google.firebase.dataconnect.FirebaseDataConnect.Queries); + method @NonNull public static com.google.firebase.dataconnect.QuerySubscription subscribe(@NonNull com.google.firebase.dataconnect.QueryRef, @NonNull String id); + method public static void update(@NonNull com.google.firebase.dataconnect.QuerySubscription, @NonNull kotlin.jvm.functions.Function1 block); } } From 21aff779fae021905967458cd036b3dd6f2f9890 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Nov 2023 16:16:18 -0500 Subject: [PATCH 053/573] getInstance() test coverage improvements --- .../dataconnect/FirebaseDataConnectTest.kt | 81 +++++++++++++++---- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 4c450734f01..190d03c358b 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -126,20 +126,24 @@ class FirebaseDataConnectTest { @Test fun getInstance_should_return_the_cached_instance_if_settings_compare_equal() { val nonDefaultApp = firebaseAppFactory.newInstance() - val settings = FirebaseDataConnectSettings.defaults.build { hostName = "TestHostName" } val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService", settings) + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService") { + hostName = "TestHostName" + } val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService", settings) + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService") { + hostName = "TestHostName" + } assertThat(instance1).isSameInstanceAs(instance2) } @Test fun getInstance_should_return_the_cached_instance_if_settings_are_null() { val nonDefaultApp = firebaseAppFactory.newInstance() - val settings = FirebaseDataConnectSettings.defaults.build { hostName = "TestHostName" } val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService", settings) + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService") { + hostName = "TestHostName" + } val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService", null) assertThat(instance1).isSameInstanceAs(instance2) @@ -148,30 +152,79 @@ class FirebaseDataConnectTest { @Test fun getInstance_should_throw_if_settings_compare_unequal_to_settings_of_cached_instance() { val nonDefaultApp = firebaseAppFactory.newInstance() - val settings1 = FirebaseDataConnectSettings.defaults.build { hostName = "HostName1" } - val settings2 = FirebaseDataConnectSettings.defaults.build { hostName = "HostName2" } val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1", settings1) + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") { + hostName = "TestHostName1" + } assertThrows(IllegalArgumentException::class.java) { - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1", settings2) + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") { + hostName = "TestHostName2" + } } val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1", settings1) + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") { + hostName = "TestHostName1" + } assertThat(instance1).isSameInstanceAs(instance2) } + @Test + fun getInstance_should_allow_different_settings_after_first_instance_is_closed() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val instance1 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService") { + hostName = "TestHostName1" + } + instance1.close() + val instance2 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService") { + hostName = "TestHostName2" + } + assertThat(instance1).isNotSameInstanceAs(instance2) + } + @Test fun getInstance_should_return_new_instance_if_settings_and_app_are_both_different() { val nonDefaultApp1 = firebaseAppFactory.newInstance() val nonDefaultApp2 = firebaseAppFactory.newInstance() - val settings1 = FirebaseDataConnectSettings.defaults.build { hostName = "HostName1" } - val settings2 = FirebaseDataConnectSettings.defaults.build { hostName = "HostName2" } val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp1, "TestLocation", "TestService", settings1) + FirebaseDataConnect.getInstance(nonDefaultApp1, "TestLocation", "TestService") { + hostName = "TestHostName1" + } + val instance2 = + FirebaseDataConnect.getInstance(nonDefaultApp2, "TestLocation", "TestService") { + hostName = "TestHostName2" + } + assertThat(instance1).isNotSameInstanceAs(instance2) + } + + @Test + fun getInstance_should_return_new_instance_if_settings_and_location_are_both_different() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val instance1 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService") { + hostName = "TestHostName1" + } + val instance2 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService") { + hostName = "TestHostName2" + } + assertThat(instance1).isNotSameInstanceAs(instance2) + } + + @Test + fun getInstance_should_return_new_instance_if_settings_and_service_are_both_different() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val instance1 = + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService1") { + hostName = "TestHostName1" + } val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp2, "TestLocation", "TestService", settings2) + FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService2") { + hostName = "TestHostName2" + } assertThat(instance1).isNotSameInstanceAs(instance2) } From 7e6053b9fb704982640d28c4c7435fd6236bf7c7 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Nov 2023 17:09:32 -0500 Subject: [PATCH 054/573] reload() improved to return Deferred; still needs performance improvement and tests --- .../firebase/dataconnect/QuerySubscription.kt | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt index 36c6e301403..f47e3c0ae65 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt @@ -40,25 +40,26 @@ internal constructor( // NOTE: The variables below must ONLY be accessed from coroutines that use `sequentialDispatcher` // for their `CoroutineDispatcher`. Having this requirement removes the need for explicitly // synchronizing access to these variables. - private var reloadInProgress = false - private var pendingReload = false + private var inProgressReload: CompletableDeferred>? = null + private var pendingReload: CompletableDeferred>? = null val lastResult: Message? get() = sharedFlow.replayCache.firstOrNull() - fun reload() { - query.dataConnect.coroutineScope.launch(sequentialDispatcher) { - pendingReload = true - if (!reloadInProgress) { - reloadInProgress = true - try { - doReloadLoop() - } finally { - reloadInProgress = false + fun reload(): Deferred> = + runBlocking(sequentialDispatcher) { + pendingReload + ?: run { + CompletableDeferred>().also { deferred -> + if (inProgressReload == null) { + inProgressReload = deferred + query.dataConnect.coroutineScope.launch(sequentialDispatcher) { doReloadLoop() } + } else { + pendingReload = deferred + } + } } - } } - } fun update(variables: VariablesType) { _variables.set(variables) @@ -69,9 +70,13 @@ internal constructor( get() = sharedFlow.asSharedFlow().onSubscription { reload() }.buffer(Channel.CONFLATED) private suspend fun doReloadLoop() { - while (pendingReload) { - pendingReload = false - sharedFlow.emit(reload(variables, query)) + while (true) { + val deferred = inProgressReload ?: break + val message = reload(variables, query) + deferred.complete(message) + sharedFlow.emit(message) + inProgressReload = pendingReload + pendingReload = null } } From 101340936c2f09bf9c9bc407204615f9732e7903 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 10 Nov 2023 10:20:36 -0500 Subject: [PATCH 055/573] Use @get:Rule instead of @JvmField @Rule --- .../google/firebase/dataconnect/FirebaseDataConnectTest.kt | 6 +++--- .../google/firebase/dataconnect/QuerySubscriptionTest.kt | 4 ++-- .../com/google/firebase/dataconnect/generated/PostsTest.kt | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 190d03c358b..811422abebd 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -40,9 +40,9 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class FirebaseDataConnectTest { - @JvmField @Rule val firebaseAppFactory = TestFirebaseAppFactory() - @JvmField @Rule val dataConnectFactory = TestDataConnectFactory() - @JvmField @Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val firebaseAppFactory = TestFirebaseAppFactory() + @get:Rule val dataConnectFactory = TestDataConnectFactory() + @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() @Test fun getInstance_without_specifying_an_app_should_use_the_default_app() { diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt index 0cf60b8bf95..931723595f0 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt @@ -40,8 +40,8 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QuerySubscriptionTest { - @JvmField @Rule val dataConnectFactory = TestDataConnectFactory() - @JvmField @Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val dataConnectFactory = TestDataConnectFactory() + @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() private lateinit var schema: PersonSchema diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt index b520147c532..bf2f94f5ff0 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt @@ -37,8 +37,8 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PostsTest { - @JvmField @Rule val dataConnectFactory = TestDataConnectFactory() - @JvmField @Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val dataConnectFactory = TestDataConnectFactory() + @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() @Test fun getPostWithNonExistingId() { From fd7c6ddc4204b4ae932f86d3847acb1141419788 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 10 Nov 2023 12:03:44 -0500 Subject: [PATCH 056/573] fix builder and add DslMarker --- .../generated/CreatePostMutation.kt | 31 ++++++++++++++++++- .../dataconnect/generated/GetPostQuery.kt | 9 +++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt index 3ee43f67ae8..555656321c1 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt @@ -20,7 +20,36 @@ import com.google.firebase.dataconnect.MutationRef class CreatePostMutation private constructor() { data class Variables(val data: PostData) { - data class PostData(val id: String, val content: String) + + val builder + get() = Builder(data = data) + + fun build(block: Builder.() -> Unit): Variables = builder.apply(block).build() + + @DslMarker annotation class VariablesDsl + + @VariablesDsl + class Builder(var data: PostData) { + fun build() = Variables(data = data) + fun data(id: String, content: String) { + data = PostData(id = id, content = content) + } + fun data(block: PostData.Builder.() -> Unit) { + data = data.build(block) + } + } + + data class PostData(val id: String, val content: String) { + val builder + get() = Builder(id = id, content = content) + + fun build(block: Builder.() -> Unit): PostData = builder.apply(block).build() + + @VariablesDsl + class Builder(var id: String, var content: String) { + fun build() = PostData(id = id, content = content) + } + } } companion object { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt index 21294b376a4..63b5b03fb40 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -22,8 +22,15 @@ import com.google.firebase.dataconnect.ResultDecodeException class GetPostQuery private constructor() { data class Variables(val id: String) { - val builder = Builder(id = id) + + val builder + get() = Builder(id = id) + fun build(block: Builder.() -> Unit): Variables = builder.apply(block).build() + + @DslMarker annotation class VariablesDsl + + @VariablesDsl class Builder(var id: String) { fun build() = Variables(id = id) } From cae49ddde556fb599dccec0b931099034ccb6997 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 14 Nov 2023 11:26:05 -0500 Subject: [PATCH 057/573] ProtoStructEncoder added (#512) --- .../firebase-dataconnect.gradle.kts | 4 + .../dataconnect/ProtoStructEncoder.kt | 232 +++++++++++ .../dataconnect/ProtoStructEncoderTest.kt | 386 ++++++++++++++++++ gradle/libs.versions.toml | 5 +- 4 files changed, 626 insertions(+), 1 deletion(-) create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts index 0ea8c5a9c59..2b16ba03d00 100644 --- a/firebase-dataconnect/firebase-dataconnect.gradle.kts +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -19,6 +19,7 @@ plugins { id("kotlin-android") id("com.google.protobuf") id("org.jetbrains.dokka") version "1.9.10" + id("org.jetbrains.kotlin.plugin.serialization") version "1.8.0" } firebaseLibrary { @@ -86,6 +87,7 @@ dependencies { api(project(":firebase-common:ktx")) api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.serialization.core) implementation(project(":firebase-annotations")) implementation(project(":firebase-components")) @@ -103,6 +105,7 @@ dependencies { testCompileOnly(libs.protobuf.java) testImplementation(libs.robolectric) testImplementation(libs.truth) + testImplementation(libs.truth.liteproto.extension) androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.androidx.test.junit) @@ -110,6 +113,7 @@ dependencies { androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.kotlin.coroutines.test) androidTestImplementation(libs.truth) + androidTestImplementation(libs.truth.liteproto.extension) } tasks.withType().all { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt new file mode 100644 index 00000000000..cc4dbf8c851 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt @@ -0,0 +1,232 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package com.google.firebase.dataconnect + +import com.google.protobuf.ListValue +import com.google.protobuf.NullValue +import com.google.protobuf.Struct +import com.google.protobuf.Value +import com.google.protobuf.Value.KindCase +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.serializer + +inline fun encodeToStruct(value: T): Struct = encodeToStruct(serializer(), value) + +fun encodeToStruct(serializer: SerializationStrategy, value: T): Struct { + val values = ProtoValueEncoder().apply { encodeSerializableValue(serializer, value) }.values + require(values.size == 1) { + "encoding produced ${values.size} Value objects, " + "but expected exactly 1" + } + val value = values.first() + require(value.hasStructValue()) { + "encoding produced ${value.kindCase}, but expected ${KindCase.STRUCT_VALUE}" + } + return value.structValue +} + +private class ProtoValueEncoder : Encoder { + + val values = mutableListOf() + + override val serializersModule = EmptySerializersModule() + + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder = + when (val kind = descriptor.kind) { + is StructureKind.MAP, + is StructureKind.CLASS -> ProtoStructValueEncoder(values) + is StructureKind.LIST -> ProtoListValueEncoder(values) + else -> throw IllegalArgumentException("unsupported SerialKind: ${kind::class.qualifiedName}") + } + + override fun encodeBoolean(value: Boolean) { + values.add(Value.newBuilder().setBoolValue(value).build()) + } + + override fun encodeByte(value: Byte) { + TODO("Not yet implemented") + } + + override fun encodeChar(value: Char) { + TODO("Not yet implemented") + } + + override fun encodeDouble(value: Double) { + values.add(Value.newBuilder().setNumberValue(value).build()) + } + + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { + TODO("Not yet implemented") + } + + override fun encodeFloat(value: Float) { + TODO("Not yet implemented") + } + + override fun encodeInline(descriptor: SerialDescriptor): Encoder { + TODO("Not yet implemented") + } + + override fun encodeInt(value: Int) { + encodeDouble(value.toDouble()) + } + + override fun encodeLong(value: Long) { + TODO("Not yet implemented") + } + + @ExperimentalSerializationApi + override fun encodeNull() { + values.add(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()) + } + + override fun encodeShort(value: Short) { + TODO("Not yet implemented") + } + + override fun encodeString(value: String) { + values.add(Value.newBuilder().setStringValue(value).build()) + } +} + +private abstract class ProtoCompositeValueEncoder(private val dest: MutableList) : + CompositeEncoder { + override val serializersModule = EmptySerializersModule() + + private val valueEncoder = ProtoValueEncoder() + private val keys = mutableListOf() + + protected abstract fun keyOf(descriptor: SerialDescriptor, index: Int): K + + override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeBoolean(value) + } + + override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeByte(value) + } + + override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeChar(value) + } + + override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeDouble(value) + } + + override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeFloat(value) + } + + override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { + keys.add(keyOf(descriptor, index)) + return valueEncoder.encodeInline(descriptor) + } + + override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeInt(value) + } + + override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeLong(value) + } + + @ExperimentalSerializationApi + override fun encodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T? + ) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeNullableSerializableValue(serializer, value) + } + + override fun encodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T + ) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeSerializableValue(serializer, value) + } + + override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeShort(value) + } + + override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { + keys.add(keyOf(descriptor, index)) + valueEncoder.encodeString(value) + } + + override fun endStructure(descriptor: SerialDescriptor) { + require(valueEncoder.values.size == keys.size) { + "internal error: " + + "valueEncoder.values.size != keys.size " + + "(${valueEncoder.values.size} != ${keys.size})" + } + + val valueByKey = + buildMap { + this@ProtoCompositeValueEncoder.keys.forEachIndexed { valueEncoderIndex, destKey -> + put(destKey, valueEncoder.values[valueEncoderIndex]) + } + } + + dest.add(Value.newBuilder().also { populate(it, valueByKey) }.build()) + } + + protected abstract fun populate(valueBuilder: Value.Builder, valueByKey: Map) +} + +private class ProtoListValueEncoder(dest: MutableList) : + ProtoCompositeValueEncoder(dest) { + override fun keyOf(descriptor: SerialDescriptor, index: Int) = index + + override fun populate(valueBuilder: Value.Builder, valueByKey: Map) { + valueBuilder.setListValue( + ListValue.newBuilder().also { listValueBuilder -> + for (i in 0 until valueByKey.size) { + listValueBuilder.addValues( + valueByKey[i] ?: throw SerializationException("list value missing at index $i") + ) + } + } + ) + } +} + +private class ProtoStructValueEncoder(dest: MutableList) : + ProtoCompositeValueEncoder(dest) { + override fun keyOf(descriptor: SerialDescriptor, index: Int) = descriptor.getElementName(index) + + override fun populate(valueBuilder: Value.Builder, valueByKey: Map) { + valueBuilder.setStructValue( + Struct.newBuilder().also { structBuilder -> + valueByKey.forEach { (key, value) -> + if (value.hasNullValue()) { + structBuilder.removeFields(key) + } else { + structBuilder.putFields(key, value) + } + } + } + ) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt new file mode 100644 index 00000000000..b273ac6f9db --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt @@ -0,0 +1,386 @@ +// Copyright 2023 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. + +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class) + +package com.google.firebase.dataconnect + +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat +import com.google.protobuf.NullValue +import com.google.protobuf.listValue +import com.google.protobuf.struct +import com.google.protobuf.value +import java.util.concurrent.atomic.AtomicLong +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.serializer +import org.junit.Assert.assertThrows +import org.junit.Test + +class ProtoStructEncoderTest { + + @Test + fun `encodeToStruct() should throw if a NUMBER_VALUE is produced`() { + val exception = assertThrows(IllegalArgumentException::class.java) { encodeToStruct(42) } + assertThat(exception).hasMessageThat().contains("NUMBER_VALUE") + } + + @Test + fun `encodeToStruct() should throw if a BOOL_VALUE is produced`() { + val exception = assertThrows(IllegalArgumentException::class.java) { encodeToStruct(true) } + assertThat(exception).hasMessageThat().contains("BOOL_VALUE") + } + + @Test + fun `encodeToStruct() should throw if a STRING_VALUE is produced`() { + val exception = + assertThrows(IllegalArgumentException::class.java) { + encodeToStruct("arbitrary string value") + } + assertThat(exception).hasMessageThat().contains("STRING_VALUE") + } + + @Test + fun `encodeToStruct() should throw if a LIST_VALUE is produced`() { + val exception = + assertThrows(IllegalArgumentException::class.java) { + encodeToStruct(listOf("element1", "element2")) + } + assertThat(exception).hasMessageThat().contains("LIST_VALUE") + } + + @Test + fun `encodeToStruct() should return an empty struct if an empty map is given`() { + val encodedStruct = encodeToStruct(emptyMap()) + assertThat(encodedStruct).isEqualToDefaultInstance() + } + + @Test + fun `encodeToStruct() should encode an class with all primitive types`() { + @Serializable + data class TestData( + val iv: Int, + val dv: Double, + val bvt: Boolean, + val bvf: Boolean, + val sv: String, + val nsvn: String?, + val nsvnn: String? + ) + val encodedStruct = + encodeToStruct( + TestData( + iv = 42, + dv = 1234.5, + bvt = true, + bvf = false, + sv = "blah blah", + nsvn = null, + nsvnn = "I'm not null" + ) + ) + + assertThat(encodedStruct) + .isEqualTo( + struct { + fields.put("iv", value { numberValue = 42.0 }) + fields.put("dv", value { numberValue = 1234.5 }) + fields.put("bvt", value { boolValue = true }) + fields.put("bvf", value { boolValue = false }) + fields.put("sv", value { stringValue = "blah blah" }) + fields.put("nsvnn", value { stringValue = "I'm not null" }) + } + ) + } + + @Test + fun `encodeToStruct() should encode lists with all primitive types`() { + @Serializable + data class TestData( + val iv: List, + val dv: List, + val bv: List, + val sv: List, + val nsv: List + ) + val encodedStruct = + encodeToStruct( + TestData( + iv = listOf(42, 43), + dv = listOf(1234.5, 5678.9), + bv = listOf(true, false, false, true), + sv = listOf("abcde", "fghij"), + nsv = listOf("klmno", null, "pqrst", null) + ) + ) + + assertThat(encodedStruct) + .isEqualTo( + struct { + fields.put( + "iv", + value { + listValue = listValue { + values.apply { + add(value { numberValue = 42.0 }) + add(value { numberValue = 43.0 }) + } + } + } + ) + fields.put( + "dv", + value { + listValue = listValue { + values.apply { + add(value { numberValue = 1234.5 }) + add(value { numberValue = 5678.9 }) + } + } + } + ) + fields.put( + "bv", + value { + listValue = listValue { + values.apply { + add(value { boolValue = true }) + add(value { boolValue = false }) + add(value { boolValue = false }) + add(value { boolValue = true }) + } + } + } + ) + fields.put( + "sv", + value { + listValue = listValue { + values.apply { + add(value { stringValue = "abcde" }) + add(value { stringValue = "fghij" }) + } + } + } + ) + fields.put( + "nsv", + value { + listValue = listValue { + values.apply { + add(value { stringValue = "klmno" }) + add(value { nullValue = NullValue.NULL_VALUE }) + add(value { stringValue = "pqrst" }) + add(value { nullValue = NullValue.NULL_VALUE }) + } + } + } + ) + } + ) + } + + @Test + fun `encodeToStruct() should support nested composite types`() { + @Serializable data class TestData3(val s: String) + @Serializable data class TestData2(val data3: TestData3, val data3N: TestData3?) + @Serializable data class TestData1(val data2: TestData2) + val encodedStruct = encodeToStruct(TestData1(TestData2(TestData3("zzzz"), null))) + + assertThat(encodedStruct) + .isEqualTo( + struct { + fields.put( + "data2", + value { + structValue = struct { + fields.put( + "data3", + value { structValue = struct { fields.put("s", value { stringValue = "zzzz" }) } } + ) + } + } + ) + } + ) + } +} + +/** + * An encoder that can be useful during testing to simply print the method invocations in order to + * discover how an encoder should be implemented. + */ +class LoggingEncoder( + private val idBySerialDescriptor: MutableMap = mutableMapOf() +) : Encoder, CompositeEncoder { + val id = nextEncoderId.incrementAndGet() + + override val serializersModule = EmptySerializersModule() + + private fun log(message: String) { + println("zzyzx LoggingEncoder[$id] $message") + } + + private fun idFor(descriptor: SerialDescriptor) = + idBySerialDescriptor[descriptor] + ?: nextSerialDescriptorId.incrementAndGet().also { idBySerialDescriptor[descriptor] = it } + + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + log( + "beginStructure() descriptorId=${idFor(descriptor)} kind=${descriptor.kind} " + + "elementsCount=${descriptor.elementsCount}" + ) + return LoggingEncoder(idBySerialDescriptor) + } + + override fun endStructure(descriptor: SerialDescriptor) { + log("endStructure() descriptorId=${idFor(descriptor)} kind=${descriptor.kind}") + } + + override fun encodeBoolean(value: Boolean) { + log("encodeBoolean($value)") + } + + override fun encodeByte(value: Byte) { + log("encodeByte($value)") + } + + override fun encodeChar(value: Char) { + log("encodeChar($value)") + } + + override fun encodeDouble(value: Double) { + log("encodeDouble($value)") + } + + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { + log("encodeEnum($index)") + } + + override fun encodeFloat(value: Float) { + log("encodeFloat($value)") + } + + override fun encodeInline(descriptor: SerialDescriptor): Encoder { + log("encodeInline() kind=${descriptor.kind} serialName=${descriptor.serialName}") + return LoggingEncoder(idBySerialDescriptor) + } + + override fun encodeInt(value: Int) { + log("encodeInt($value)") + } + + override fun encodeLong(value: Long) { + log("encodeLong($value)") + } + + @ExperimentalSerializationApi + override fun encodeNull() { + log("encodeNull()") + } + + override fun encodeShort(value: Short) { + log("encodeShort($value)") + } + + override fun encodeString(value: String) { + log("encodeString($value)") + } + + override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { + log("encodeBooleanElement($value) index=$index elementName=${descriptor.getElementName(index)}") + } + + override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { + log("encodeByteElement($value) index=$index elementName=${descriptor.getElementName(index)}") + } + + override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { + log("encodeCharElement($value) index=$index elementName=${descriptor.getElementName(index)}") + } + + override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { + log("encodeDoubleElement($value) index=$index elementName=${descriptor.getElementName(index)}") + } + + override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { + log("encodeFloatElement($value) index=$index elementName=${descriptor.getElementName(index)}") + } + + override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { + log("encodeInlineElement() index=$index elementName=${descriptor.getElementName(index)}") + return this + } + + override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { + log("encodeIntElement($value) index=$index elementName=${descriptor.getElementName(index)}") + } + + override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { + log("encodeLongElement($value) index=$index elementName=${descriptor.getElementName(index)}") + } + + override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { + log("encodeShortElement($value) index=$index elementName=${descriptor.getElementName(index)}") + } + + override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { + log("encodeStringElement($value) index=$index elementName=${descriptor.getElementName(index)}") + } + + @ExperimentalSerializationApi + override fun encodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T? + ) { + log( + "encodeNullableSerializableElement($value) index=$index elementName=${descriptor.getElementName(index)}" + ) + if (value != null) { + encodeSerializableValue(serializer, value) + } + } + + override fun encodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T + ) { + log( + "encodeSerializableElement($value) index=$index elementName=${descriptor.getElementName(index)}" + ) + encodeSerializableValue(serializer, value) + } + + companion object { + + fun encode(serializer: SerializationStrategy, value: T) { + LoggingEncoder().encodeSerializableValue(serializer, value) + } + + inline fun encode(value: T) = encode(serializer(), value) + + private val nextEncoderId = AtomicLong(0) + private val nextSerialDescriptorId = AtomicLong(998800000L) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d1811f353d8..89f4ee95910 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ truth = "1.1.2" protobufjavautil = "3.24.0" kotest = "5.5.5" quickcheck = "0.6" +serialization = "1.5.0" androidx-test-core="1.5.0" androidx-test-junit="1.1.5" @@ -45,7 +46,8 @@ javax-annotation-jsr250 = { module = "javax.annotation:jsr250-api", version = "1 javax-inject = { module = "javax.inject:javax.inject", version = "1" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-coroutines-tasks = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutines" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.5.0" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } okhttp = { module = "com.squareup.okhttp3:okhttp", version = "3.12.13" } org-json = { module = "org.json:json", version = "20210307" } @@ -68,6 +70,7 @@ mockito-core = { module = "org.mockito:mockito-core", version = "2.28.2" } mockito-dexmaker = { module = "com.linkedin.dexmaker:dexmaker-mockito", version = "2.28.3" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } truth = { module = "com.google.truth:truth", version.ref = "truth" } +truth-liteproto-extension = { module = "com.google.truth.extensions:truth-liteproto-extension", version.ref = "truth" } protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobufjavautil" } kotest-runner = { module = "io.kotest:kotest-runner-junit4-jvm", version.ref = "kotest" } kotest-assertions = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" } From 5ac624534a4c77eedb2106ea5e90bae5a4166b84 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 15 Nov 2023 12:21:17 -0500 Subject: [PATCH 058/573] add firemat emulator files --- .../scripts/emulator/.gitignore | 1 + .../emulator/api/operations/mutations.gql | 10 +++++++++ .../emulator/api/operations/queries.gql | 21 +++++++++++++++++++ .../scripts/emulator/api/schema/schema.gql | 10 +++++++++ .../scripts/emulator/firebase.json | 14 +++++++++++++ .../scripts/emulator/firemat.yaml | 7 +++++++ 6 files changed, 63 insertions(+) create mode 100644 firebase-dataconnect/scripts/emulator/.gitignore create mode 100644 firebase-dataconnect/scripts/emulator/api/operations/mutations.gql create mode 100644 firebase-dataconnect/scripts/emulator/api/operations/queries.gql create mode 100644 firebase-dataconnect/scripts/emulator/api/schema/schema.gql create mode 100644 firebase-dataconnect/scripts/emulator/firebase.json create mode 100644 firebase-dataconnect/scripts/emulator/firemat.yaml diff --git a/firebase-dataconnect/scripts/emulator/.gitignore b/firebase-dataconnect/scripts/emulator/.gitignore new file mode 100644 index 00000000000..7b6b1294705 --- /dev/null +++ b/firebase-dataconnect/scripts/emulator/.gitignore @@ -0,0 +1 @@ +.dataconnect/ diff --git a/firebase-dataconnect/scripts/emulator/api/operations/mutations.gql b/firebase-dataconnect/scripts/emulator/api/operations/mutations.gql new file mode 100644 index 00000000000..36c47197fd3 --- /dev/null +++ b/firebase-dataconnect/scripts/emulator/api/operations/mutations.gql @@ -0,0 +1,10 @@ +mutation createPost($data: Post_Data! @pick(fields: ["id", "content"])) @auth(is: PUBLIC) { + post_insert(data: $data) +} +mutation deletePost($id: String!) @auth(is: PUBLIC) { + post_delete(id: $id) +} + +mutation createComment($data: Comment_Data! @pick(fields: ["content", "postId"])) @auth(is: PUBLIC) { + comment_insert(data: $data) +} \ No newline at end of file diff --git a/firebase-dataconnect/scripts/emulator/api/operations/queries.gql b/firebase-dataconnect/scripts/emulator/api/operations/queries.gql new file mode 100644 index 00000000000..02b99f0723f --- /dev/null +++ b/firebase-dataconnect/scripts/emulator/api/operations/queries.gql @@ -0,0 +1,21 @@ +query getPost($id: String!) @auth(is: PUBLIC) { + post(id: $id) { + content + comments: comments_as_post { + id + content + } + } +} +query listPosts @auth(is: PUBLIC) { + posts { + id + content + } +} + +query listPostsOnlyId @auth(is: PUBLIC) { + posts { + id + } +} \ No newline at end of file diff --git a/firebase-dataconnect/scripts/emulator/api/schema/schema.gql b/firebase-dataconnect/scripts/emulator/api/schema/schema.gql new file mode 100644 index 00000000000..ed72f8bd32f --- /dev/null +++ b/firebase-dataconnect/scripts/emulator/api/schema/schema.gql @@ -0,0 +1,10 @@ +type Post @table { + id: String! + content: String! +} + +type Comment @table { + id: String! + content: String! + post: Post! +} \ No newline at end of file diff --git a/firebase-dataconnect/scripts/emulator/firebase.json b/firebase-dataconnect/scripts/emulator/firebase.json new file mode 100644 index 00000000000..27491666654 --- /dev/null +++ b/firebase-dataconnect/scripts/emulator/firebase.json @@ -0,0 +1,14 @@ +{ + "firemat": { + "source": "api" + }, + "emulators": { + "firemat": { + "port": 9399 + }, + "ui": { + "enabled": false + }, + "singleProjectMode": true + } +} diff --git a/firebase-dataconnect/scripts/emulator/firemat.yaml b/firebase-dataconnect/scripts/emulator/firemat.yaml new file mode 100644 index 00000000000..60d04ecaac9 --- /dev/null +++ b/firebase-dataconnect/scripts/emulator/firemat.yaml @@ -0,0 +1,7 @@ +specVersion: v1alpha +schema: + main: + source: ./api/schema +operationSet: + crud: + source: ./api/operations From eacf678ec9480ab70e268eb20247a21e9a57b13d Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 16 Nov 2023 10:54:20 -0500 Subject: [PATCH 059/573] Use serializer to convert variables to proto struct (#513) --- .../dataconnect/FirebaseDataConnectTest.kt | 52 +++++++------- .../dataconnect/testutil/IdentityCodec.kt | 3 +- .../testutil/schemas/PersonSchema.kt | 68 ++++++++----------- .../google/firebase/dataconnect/BaseRef.kt | 8 ++- .../dataconnect/DataConnectGrpcClient.kt | 54 +++------------ .../dataconnect/FirebaseDataConnect.kt | 27 ++++++-- .../firebase/dataconnect/MutationRef.kt | 6 +- .../dataconnect/ProtoStructEncoder.kt | 66 +++++++++++++++++- .../google/firebase/dataconnect/QueryRef.kt | 6 +- .../apiproposal/QueryApiProposal.kt | 10 +-- .../generated/CreatePostMutation.kt | 12 ++-- .../dataconnect/generated/GetPostQuery.kt | 10 +-- .../dataconnect/ProtoStructEncoderTest.kt | 6 ++ 13 files changed, 191 insertions(+), 137 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 811422abebd..e393a1e6855 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -32,6 +32,8 @@ import kotlin.concurrent.thread import kotlin.concurrent.withLock import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test @@ -307,16 +309,21 @@ class FirebaseDataConnectTest { val postId = "${UUID.randomUUID()}" val postContent = "${System.currentTimeMillis()}" + @Serializable data class PostData(val id: String, val content: String) + @Serializable data class CreatePostVariables(val data: PostData) + @Serializable data class GetPostVariables(val id: String) + run { val mutation = - dc.mutation( + dc.mutation>( operationName = "createPost", operationSet = "crud", revision = "TestRevision", codec = IdentityCodec, + variablesSerializer = serializer(), ) val mutationResponse = - mutation.execute(mapOf("data" to mapOf("id" to postId, "content" to postContent))) + mutation.execute(CreatePostVariables(PostData(id = postId, content = postContent))) assertWithMessage("mutationResponse") .that(mutationResponse) .containsExactlyEntriesIn(mapOf("post_insert" to null)) @@ -324,13 +331,14 @@ class FirebaseDataConnectTest { run { val query = - dc.query( + dc.query>( operationName = "getPost", operationSet = "crud", revision = "TestRevision", - codec = IdentityCodec + codec = IdentityCodec, + variablesSerializer = serializer(), ) - val queryResult = query.execute(mapOf("id" to postId)) + val queryResult = query.execute(GetPostVariables(id = postId)) assertWithMessage("queryResponse") .that(queryResult) .containsExactlyEntriesIn( @@ -341,41 +349,39 @@ class FirebaseDataConnectTest { @Test fun testInstallEmulatorSchema() { + @Serializable data class PersonData(val id: String, val name: String, val age: Int? = null) + @Serializable data class CreatePersonVariables(val data: PersonData) + @Serializable data class GetPersonVariables(val id: String) + suspend fun FirebaseDataConnect.createPerson(id: String, name: String, age: Int? = null) = - mutation( + mutation>( operationName = "createPerson", operationSet = "ops", revision = "42", - codec = IdentityCodec - ) - .execute( - mapOf( - "data" to - buildMap { - put("id", id) - put("name", name) - age?.let { put("age", it) } - } - ) + codec = IdentityCodec, + variablesSerializer = serializer(), ) + .execute(CreatePersonVariables(PersonData(id = id, name = name, age = age))) suspend fun FirebaseDataConnect.getPerson(id: String) = - query( + query>( operationName = "getPerson", operationSet = "ops", revision = "42", - codec = IdentityCodec + codec = IdentityCodec, + variablesSerializer = serializer(), ) - .execute(mapOf("id" to id)) + .execute(GetPersonVariables(id = id)) suspend fun FirebaseDataConnect.getAllPeople() = - query( + query>( operationName = "getAllPeople", operationSet = "ops", revision = "42", - codec = IdentityCodec + codec = IdentityCodec, + variablesSerializer = serializer(), ) - .execute(emptyMap()) + .execute(Unit) fun Map<*, *>.assertEqualsGetPersonResponse(name: String, age: Double?) { assertThat(keys).containsExactly("person") diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/IdentityCodec.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/IdentityCodec.kt index d208e731bf8..726221c0b9e 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/IdentityCodec.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/IdentityCodec.kt @@ -2,7 +2,6 @@ package com.google.firebase.dataconnect.testutil import com.google.firebase.dataconnect.BaseRef -object IdentityCodec : BaseRef.Codec, Map> { - override fun encodeVariables(variables: Map): Map = variables +object IdentityCodec : BaseRef.Codec> { override fun decodeResult(map: Map): Map = map } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index f1dcd9bd1a3..06d39da43ec 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -19,6 +19,8 @@ import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.MutationRef import com.google.firebase.dataconnect.QueryRef import com.google.firebase.dataconnect.testutil.installEmulatorSchema +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer class PersonSchema(val dataConnect: FirebaseDataConnect) { @@ -32,25 +34,15 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { operationSet = "ops", revision = "42", codec = CreatePersonMutation, + variablesSerializer = serializer(), ) class CreatePersonMutation private constructor() { - data class Variables(val id: String, val name: String, val age: Int? = null) - - internal companion object Codec : BaseRef.Codec { - override fun encodeVariables(variables: Variables): Map = - variables.run { - mapOf( - "data" to - buildMap { - put("id", id) - put("name", name) - age?.let { put("age", it) } - } - ) - } + @Serializable data class PersonData(val id: String, val name: String, val age: Int? = null) + @Serializable data class Variables(val data: PersonData) + internal companion object Codec : BaseRef.Codec { override fun decodeResult(map: Map) {} } } @@ -61,26 +53,15 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { operationSet = "ops", revision = "42", codec = UpdatePersonMutation, + variablesSerializer = serializer(), ) class UpdatePersonMutation private constructor() { - data class Variables(val id: String, val name: String? = null, val age: Int? = null) - - internal companion object Codec : BaseRef.Codec { - - override fun encodeVariables(variables: Variables): Map = - variables.run { - mapOf( - "id" to id, - "data" to - buildMap { - name?.let { put("name", it) } - age?.let { put("age", it) } - } - ) - } + @Serializable data class PersonData(val name: String? = null, val age: Int? = null) + @Serializable data class Variables(val id: String, val data: PersonData) + internal companion object Codec : BaseRef.Codec { override fun decodeResult(map: Map) {} } } @@ -91,16 +72,14 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { operationSet = "ops", revision = "42", codec = DeletePersonMutation, + variablesSerializer = serializer(), ) class DeletePersonMutation private constructor() { - data class Variables(val id: String) - - internal companion object Codec : BaseRef.Codec { - - override fun encodeVariables(variables: Variables) = variables.run { mapOf("id" to id) } + @Serializable data class Variables(val id: String) + internal companion object Codec : BaseRef.Codec { override fun decodeResult(map: Map) {} } } @@ -111,16 +90,16 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { operationSet = "ops", revision = "42", codec = GetPersonQuery, + variablesSerializer = serializer(), ) class GetPersonQuery private constructor() { + @Serializable data class Variables(val id: String, val name: String? = null, val age: Int? = null) data class Result(val name: String, val age: Int? = null) - internal companion object Codec : BaseRef.Codec { - override fun encodeVariables(variables: Variables) = variables.run { mapOf("id" to id) } - + internal companion object Codec : BaseRef.Codec { override fun decodeResult(map: Map) = (map["person"] as Map<*, *>?)?.let { Result(name = it["name"] as String, age = (it["age"] as Double?)?.toInt()) @@ -134,7 +113,12 @@ object CreatePersonMutationExt { id: String, name: String, age: Int? - ) = execute(PersonSchema.CreatePersonMutation.Variables(id = id, name = name, age = age)) + ) = + execute( + PersonSchema.CreatePersonMutation.Variables( + PersonSchema.CreatePersonMutation.PersonData(id = id, name = name, age = age) + ) + ) } object UpdatePersonMutationExt { @@ -142,7 +126,13 @@ object UpdatePersonMutationExt { id: String, name: String? = null, age: Int? = null - ) = execute(PersonSchema.UpdatePersonMutation.Variables(id = id, name = name, age = age)) + ) = + execute( + PersonSchema.UpdatePersonMutation.Variables( + id = id, + data = PersonSchema.UpdatePersonMutation.PersonData(name = name, age = age) + ) + ) } object DeletePersonMutationExt { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt index 3e592e96c59..755a08dc2a7 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt @@ -13,18 +13,20 @@ // limitations under the License. package com.google.firebase.dataconnect +import kotlinx.serialization.SerializationStrategy + abstract class BaseRef internal constructor( val dataConnect: FirebaseDataConnect, internal val operationName: String, internal val operationSet: String, internal val revision: String, - internal val codec: Codec, + internal val codec: Codec, + internal val variablesSerializer: SerializationStrategy, ) { abstract suspend fun execute(variables: VariablesType): ResultType - interface Codec { - fun encodeVariables(variables: VariablesType): Map + interface Codec { fun decodeResult(map: Map): ResultType } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index a76e4379b96..11ecb3d1a34 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -15,12 +15,8 @@ package com.google.firebase.dataconnect import android.content.Context import com.google.android.gms.security.ProviderInstaller -import com.google.protobuf.NullValue import com.google.protobuf.Struct import com.google.protobuf.Value -import com.google.protobuf.listValue -import com.google.protobuf.struct -import com.google.protobuf.value import google.internal.firebase.firemat.v0.DataServiceGrpcKt.DataServiceCoroutineStub import google.internal.firebase.firemat.v0.executeMutationRequest import google.internal.firebase.firemat.v0.executeQueryRequest @@ -29,6 +25,7 @@ import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder import java.util.concurrent.Executor import java.util.concurrent.TimeUnit +import kotlinx.serialization.SerializationStrategy internal class DataConnectGrpcClient( val context: Context, @@ -82,16 +79,17 @@ internal class DataConnectGrpcClient( private val grpcStub: DataServiceCoroutineStub by lazy { DataServiceCoroutineStub(grpcChannel) } - suspend fun executeQuery( + suspend fun executeQuery( operationSet: String, operationName: String, revision: String, - variables: Map, + variables: V, + variablesSerializer: SerializationStrategy ): Map { val request = executeQueryRequest { this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName - this.variables = structFromMap(variables) + this.variables = encodeToStruct(variablesSerializer, variables) } logger.debug { "executeQuery() sending request: $request" } @@ -112,16 +110,17 @@ internal class DataConnectGrpcClient( return mapFromStruct(response.data) } - suspend fun executeMutation( + suspend fun executeMutation( operationSet: String, operationName: String, revision: String, - variables: Map + variables: V, + variablesSerializer: SerializationStrategy ): Map { val request = executeMutationRequest { this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName - this.variables = structFromMap(variables) + this.variables = encodeToStruct(variablesSerializer, variables) } logger.debug { "executeMutation() sending request: $request" } @@ -174,38 +173,3 @@ private fun objectFromStructValue(struct: Value): Any? = else -> throw ResultDecodeException("unsupported Struct kind: $kindCase") } } - -private fun structFromMap(map: Map) = struct { - map.keys.sorted().forEach { key -> fields.put(key, valueFromObject(map[key])) } -} - -private fun valueFromObject(obj: Any?): Value = value { - when (obj) { - null -> nullValue = NullValue.NULL_VALUE - is String -> stringValue = obj - is Boolean -> boolValue = obj - is Int -> numberValue = obj.toDouble() - is Double -> numberValue = obj - is Map<*, *> -> - structValue = - obj.let { - struct { - it.forEach { entry -> - val key = - entry.key.let { key -> - key as? String - ?: throw ResultDecodeException( - "unsupported map key: " + - if (key == null) "null" else "${key::class.qualifiedName} (${key})" - ) - } - fields.put(key, valueFromObject(entry.value)) - } - } - } - is Iterable<*> -> - listValue = obj.let { listValue { it.forEach { values.add(valueFromObject(it)) } } } - else -> - throw ResultDecodeException("unsupported value type: ${obj::class.qualifiedName} ($obj)") - } -} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 803d7e50ea2..376b97f4139 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -20,7 +20,14 @@ import com.google.firebase.app import com.google.firebase.concurrent.FirebaseExecutors import java.io.Closeable import java.util.concurrent.Executor -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.serialization.SerializationStrategy class FirebaseDataConnect internal constructor( @@ -91,7 +98,8 @@ internal constructor( operationName = ref.operationName, operationSet = ref.operationSet, revision = ref.revision, - variables = ref.codec.encodeVariables(variables) + variables = variables, + variablesSerializer = ref.variablesSerializer, ) ) } @@ -104,7 +112,8 @@ internal constructor( operationName = ref.operationName, operationSet = ref.operationSet, revision = ref.revision, - variables = ref.codec.encodeVariables(variables) + variables = variables, + variablesSerializer = ref.variablesSerializer, ) ) } @@ -113,28 +122,32 @@ internal constructor( operationName: String, operationSet: String, revision: String, - codec: BaseRef.Codec + codec: BaseRef.Codec, + variablesSerializer: SerializationStrategy, ): QueryRef = QueryRef( dataConnect = this, operationName = operationName, operationSet = operationSet, revision = revision, - codec = codec + codec = codec, + variablesSerializer = variablesSerializer, ) fun mutation( operationName: String, operationSet: String, revision: String, - codec: BaseRef.Codec + codec: BaseRef.Codec, + variablesSerializer: SerializationStrategy, ): MutationRef = MutationRef( dataConnect = this, operationName = operationName, operationSet = operationSet, revision = revision, - codec = codec + codec = codec, + variablesSerializer = variablesSerializer, ) override fun close() { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt index 76ca038e891..053eecefc10 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt @@ -13,13 +13,16 @@ // limitations under the License. package com.google.firebase.dataconnect +import kotlinx.serialization.SerializationStrategy + class MutationRef internal constructor( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, revision: String, - codec: Codec, + codec: Codec, + variablesSerializer: SerializationStrategy ) : BaseRef( dataConnect = dataConnect, @@ -27,6 +30,7 @@ internal constructor( operationSet = operationSet, revision = revision, codec = codec, + variablesSerializer = variablesSerializer, ) { override suspend fun execute(variables: VariablesType): ResultType = dataConnect.executeMutation(this, variables) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt index cc4dbf8c851..eb4f725c066 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt @@ -17,12 +17,15 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer -inline fun encodeToStruct(value: T): Struct = encodeToStruct(serializer(), value) +inline fun encodeToStruct(value: T): Struct = encodeToStruct(serializer(), value) -fun encodeToStruct(serializer: SerializationStrategy, value: T): Struct { +fun encodeToStruct(serializer: SerializationStrategy, value: T): Struct { val values = ProtoValueEncoder().apply { encodeSerializableValue(serializer, value) }.values + if (values.isEmpty()) { + return Struct.getDefaultInstance() + } require(values.size == 1) { - "encoding produced ${values.size} Value objects, " + "but expected exactly 1" + "encoding produced ${values.size} Value objects, " + "but expected at most 1" } val value = values.first() require(value.hasStructValue()) { @@ -42,6 +45,7 @@ private class ProtoValueEncoder : Encoder { is StructureKind.MAP, is StructureKind.CLASS -> ProtoStructValueEncoder(values) is StructureKind.LIST -> ProtoListValueEncoder(values) + is StructureKind.OBJECT -> ProtoObjectValueEncoder else -> throw IllegalArgumentException("unsupported SerialKind: ${kind::class.qualifiedName}") } @@ -230,3 +234,59 @@ private class ProtoStructValueEncoder(dest: MutableList) : ) } } + +private object ProtoObjectValueEncoder : CompositeEncoder { + override val serializersModule = EmptySerializersModule() + + override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) = + notSupported() + + override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) = + notSupported() + + override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) = + notSupported() + + override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = + notSupported() + + override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) = + notSupported() + + override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder = + notSupported() + + override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) = + notSupported() + + override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) = + notSupported() + + @ExperimentalSerializationApi + override fun encodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T? + ) = notSupported() + + override fun encodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T + ) = notSupported() + + override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) = + notSupported() + + override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) = + notSupported() + + private fun notSupported(): Nothing = + throw UnsupportedOperationException( + "The only valid method call on ProtoObjectValueEncoder is endStructure()" + ) + + override fun endStructure(descriptor: SerialDescriptor) {} +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt index 100e17ac563..3302268de44 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt @@ -13,13 +13,16 @@ // limitations under the License. package com.google.firebase.dataconnect +import kotlinx.serialization.SerializationStrategy + class QueryRef internal constructor( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, revision: String, - codec: Codec, + codec: Codec, + variablesSerializer: SerializationStrategy, ) : BaseRef( dataConnect = dataConnect, @@ -27,6 +30,7 @@ internal constructor( operationSet = operationSet, revision = revision, codec = codec, + variablesSerializer = variablesSerializer, ) { override suspend fun execute(variables: VariablesType): ResultType = dataConnect.executeQuery(this, variables) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index 899b654b4c6..1e77ab719b8 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationStrategy //////////////////////////////////////////////////////////////////////////////////////////////////// // CORE SDK @@ -16,7 +18,8 @@ class FirebaseDataConnect { operationName: String, operationSet: String, revision: String, - codec: BaseRef.Codec + codec: BaseRef.Codec, + variablesSerializer: SerializationStrategy, ): QueryRef = TODO() class Queries internal constructor() { @@ -43,8 +46,7 @@ abstract class BaseRef internal constructor() { abstract suspend fun execute(variables: VariablesType): ResultType - interface Codec { - fun encodeVariables(variables: VariablesType): Map + interface Codec { fun decodeResult(map: Map): ResultType } } @@ -90,7 +92,7 @@ class QuerySubscription internal constructor() { class GetPostQuery private constructor() { - data class Variables(val id: String) + @Serializable data class Variables(val id: String) data class Result(val post: Post) { data class Post(val content: String, val comments: List) { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt index 555656321c1..66cb538fe6f 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt @@ -16,9 +16,12 @@ package com.google.firebase.dataconnect.generated import com.google.firebase.dataconnect.BaseRef import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.MutationRef +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer class CreatePostMutation private constructor() { + @Serializable data class Variables(val data: PostData) { val builder @@ -39,6 +42,7 @@ class CreatePostMutation private constructor() { } } + @Serializable data class PostData(val id: String, val content: String) { val builder get() = Builder(id = id, content = content) @@ -59,14 +63,12 @@ class CreatePostMutation private constructor() { operationName = "createPost", operationSet = "crud", revision = "1234567890abcdef", - codec = codec + codec = codec, + variablesSerializer = serializer() ) private val codec = - object : BaseRef.Codec { - override fun encodeVariables(variables: Variables) = - mapOf("data" to variables.data.run { mapOf("id" to id, "content" to content) }) - + object : BaseRef.Codec { override fun decodeResult(map: Map) {} } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt index 63b5b03fb40..4332d127ea4 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -18,9 +18,12 @@ import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.QueryRef import com.google.firebase.dataconnect.QuerySubscription import com.google.firebase.dataconnect.ResultDecodeException +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer class GetPostQuery private constructor() { + @Serializable data class Variables(val id: String) { val builder @@ -49,13 +52,12 @@ class GetPostQuery private constructor() { operationName = "getPost", operationSet = "crud", revision = "1234567890abcdef", - codec = codec + codec = codec, + variablesSerializer = serializer(), ) private val codec = - object : BaseRef.Codec { - override fun encodeVariables(variables: Variables) = mapOf("id" to variables.id) - + object : BaseRef.Codec { override fun decodeResult(map: Map) = map["post"].let { if (it == null) { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt index b273ac6f9db..aa78f645ff5 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt @@ -72,6 +72,12 @@ class ProtoStructEncoderTest { assertThat(encodedStruct).isEqualToDefaultInstance() } + @Test + fun `encodeToStruct() should encode Unit as an empty struct`() { + val encodedStruct = encodeToStruct(Unit) + assertThat(encodedStruct).isEqualToDefaultInstance() + } + @Test fun `encodeToStruct() should encode an class with all primitive types`() { @Serializable From 3f7925fa17d45a405d8310321d2e16c406aa59dc Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 16 Nov 2023 13:13:31 -0500 Subject: [PATCH 060/573] PersonSchema.kt: add getAllPeople query --- .../testutil/schemas/PersonSchema.kt | 38 ++++++ .../testutil/schemas/PersonSchemaTest.kt | 110 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index 06d39da43ec..86f0cf1bad9 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -106,6 +106,39 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { } } } + + val getAllPeople = + dataConnect.query( + operationName = "getAllPeople", + operationSet = "ops", + revision = "42", + codec = GetAllPeopleQuery, + variablesSerializer = serializer(), + ) + + class GetAllPeopleQuery private constructor() { + + data class Person(val id: String, val name: String, val age: Int?) + data class Result(val people: List) + + internal companion object Codec : BaseRef.Codec { + override fun decodeResult(map: Map) = + (map["people"] as List<*>).let { people -> + Result( + people = + people + .map { it as Map<*, *> } + .map { + Person( + id = it["id"] as String, + name = it["name"] as String, + age = (it["age"] as Double?)?.toInt() + ) + } + ) + } + } + } } object CreatePersonMutationExt { @@ -147,3 +180,8 @@ object GetPersonQueryExt { fun QueryRef .subscribe(id: String) = subscribe(PersonSchema.GetPersonQuery.Variables(id = id)) } + +object GetAllPeoplePersonQueryExt { + suspend fun QueryRef.execute() = execute(Unit) + fun QueryRef.subscribe() = subscribe(Unit) +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt new file mode 100644 index 00000000000..c520ed72e6b --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt @@ -0,0 +1,110 @@ +package com.google.firebase.dataconnect.testutil.schemas + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule +import com.google.firebase.dataconnect.testutil.TestDataConnectFactory +import com.google.firebase.dataconnect.testutil.schemas.CreatePersonMutationExt.execute +import com.google.firebase.dataconnect.testutil.schemas.DeletePersonMutationExt.execute +import com.google.firebase.dataconnect.testutil.schemas.GetAllPeoplePersonQueryExt.execute +import com.google.firebase.dataconnect.testutil.schemas.GetPersonQueryExt.execute +import com.google.firebase.dataconnect.testutil.schemas.UpdatePersonMutationExt.execute +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PersonSchemaTest { + + @get:Rule val dataConnectFactory = TestDataConnectFactory() + @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + + private lateinit var schema: PersonSchema + + @Before + fun initializePersonSchema() { + schema = PersonSchema(dataConnectFactory.newInstance()) + runBlocking { schema.installEmulatorSchema() } + } + + @Test + fun createPersonShouldCreateTheSpecifiedPerson(): Unit = runBlocking { + schema.createPerson.execute(id = "1234", name = "TestName", age = 42) + + val person = schema.getPerson.execute(id = "1234") + + assertThat(person?.name).isEqualTo("TestName") + assertThat(person?.age).isEqualTo(42) + } + + @Test + fun deletePersonShouldDeleteTheSpecifiedPerson(): Unit = runBlocking { + schema.createPerson.execute(id = "1234", name = "TestName", age = 42) + assertThat(schema.getPerson.execute(id = "1234")).isNotNull() + + schema.deletePerson.execute(id = "1234") + + assertThat(schema.getPerson.execute(id = "1234")).isNull() + } + + @Test + fun updatePersonShouldUpdateTheSpecifiedPerson(): Unit = runBlocking { + schema.createPerson.execute(id = "1234", name = "TestName0", age = 42) + + schema.updatePerson.execute(id = "1234", name = "TestName99", age = 999) + + val person = schema.getPerson.execute(id = "1234") + assertThat(person?.name).isEqualTo("TestName99") + assertThat(person?.age).isEqualTo(999) + } + + @Test + fun getPersonShouldReturnThePersonWithTheSpecifiedId(): Unit = runBlocking { + schema.createPerson.execute(id = "111", name = "Name111", age = 111) + schema.createPerson.execute(id = "222", name = "Name222", age = 222) + schema.createPerson.execute(id = "333", name = "Name333", age = null) + + val person1 = schema.getPerson.execute(id = "111") + val person2 = schema.getPerson.execute(id = "222") + val person3 = schema.getPerson.execute(id = "333") + + assertThat(person1?.name).isEqualTo("Name111") + assertThat(person1?.age).isEqualTo(111) + assertThat(person2?.name).isEqualTo("Name222") + assertThat(person2?.age).isEqualTo(222) + assertThat(person3?.name).isEqualTo("Name333") + assertThat(person3?.age).isNull() + } + + @Test + fun getPersonShouldReturnNullIfThePersonDoesNotExist(): Unit = runBlocking { + schema.createPerson.execute(id = "111", name = "Name111", age = 111) + + val person = schema.getPerson.execute(id = "IdOfPersonThatDoesNotExit") + + assertThat(person).isNull() + } + + @Test + fun getAllPeopleShouldReturnEmptyListIfTheDatabaseIsEmpty(): Unit = runBlocking { + assertThat(schema.getAllPeople.execute().people).isEmpty() + } + + @Test + fun getAllPeopleShouldReturnAllPeopleInTheDatabase(): Unit = runBlocking { + schema.createPerson.execute(id = "111", name = "Name111", age = 111) + schema.createPerson.execute(id = "222", name = "Name222", age = 222) + schema.createPerson.execute(id = "333", name = "Name333", age = null) + + val result = schema.getAllPeople.execute() + + assertThat(result.people) + .containsExactly( + PersonSchema.GetAllPeopleQuery.Person(id = "111", name = "Name111", age = 111), + PersonSchema.GetAllPeopleQuery.Person(id = "222", name = "Name222", age = 222), + PersonSchema.GetAllPeopleQuery.Person(id = "333", name = "Name333", age = null), + ) + } +} From 46521b88dd261c6781597d892c613674e99f9c6a Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 16 Nov 2023 13:13:58 -0500 Subject: [PATCH 061/573] ProtoStructEncoderTest.kt: minor fixes --- .../com/google/firebase/dataconnect/ProtoStructEncoderTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt index aa78f645ff5..575eb755b20 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt @@ -233,7 +233,7 @@ class ProtoStructEncoderTest { * An encoder that can be useful during testing to simply print the method invocations in order to * discover how an encoder should be implemented. */ -class LoggingEncoder( +private class LoggingEncoder( private val idBySerialDescriptor: MutableMap = mutableMapOf() ) : Encoder, CompositeEncoder { val id = nextEncoderId.incrementAndGet() @@ -332,7 +332,7 @@ class LoggingEncoder( override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { log("encodeInlineElement() index=$index elementName=${descriptor.getElementName(index)}") - return this + return LoggingEncoder(idBySerialDescriptor) } override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { From 2397d4d18a9e59445ca6acffa6a236fdafb7ea19 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 16 Nov 2023 13:23:30 -0500 Subject: [PATCH 062/573] ProtoStructDecoder.kt skeleton added --- .../dataconnect/ProtoStructDecoder.kt | 80 +++++ .../dataconnect/ProtoStructDecoderTest.kt | 280 ++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt new file mode 100644 index 00000000000..800d2645e89 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt @@ -0,0 +1,80 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package com.google.firebase.dataconnect + +import com.google.protobuf.Struct +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.serializer + +inline fun decodeFromStruct(struct: Struct): T = decodeFromStruct(serializer(), struct) + +fun decodeFromStruct(serializer: DeserializationStrategy, struct: Struct): T { + return ProtoValueDecoder(struct).decodeSerializableValue(serializer) +} + +private class ProtoValueDecoder(private val struct: Struct) : Decoder { + override val serializersModule = EmptySerializersModule() + + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + TODO("Not yet implemented") + } + + override fun decodeBoolean(): Boolean { + TODO("Not yet implemented") + } + + override fun decodeByte(): Byte { + TODO("Not yet implemented") + } + + override fun decodeChar(): Char { + TODO("Not yet implemented") + } + + override fun decodeDouble(): Double { + TODO("Not yet implemented") + } + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { + TODO("Not yet implemented") + } + + override fun decodeFloat(): Float { + TODO("Not yet implemented") + } + + override fun decodeInline(descriptor: SerialDescriptor): Decoder { + TODO("Not yet implemented") + } + + override fun decodeInt(): Int { + TODO("Not yet implemented") + } + + override fun decodeLong(): Long { + TODO("Not yet implemented") + } + + @ExperimentalSerializationApi + override fun decodeNotNullMark(): Boolean { + TODO("Not yet implemented") + } + + @ExperimentalSerializationApi + override fun decodeNull(): Nothing? { + TODO("Not yet implemented") + } + + override fun decodeShort(): Short { + TODO("Not yet implemented") + } + + override fun decodeString(): String { + TODO("Not yet implemented") + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt new file mode 100644 index 00000000000..402d11173c8 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt @@ -0,0 +1,280 @@ +// Copyright 2023 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. + +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class) + +package com.google.firebase.dataconnect + +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.atomic.AtomicLong +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.serializer +import org.junit.Test + +class ProtoStructDecoderTest { + + @Test + fun `decodeFromStruct() can decode a Struct with a single String value`() { + @Serializable data class TestData(val value: String) + val struct = encodeToStruct(TestData(value = "Test Value")) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(value = "Test Value")) + } +} + +/** + * A decoder that can be useful during testing to simply print the method invocations in order to + * discover how a decoder should be implemented. + */ +private class LoggingDecoder( + value: Any?, + private val idBySerialDescriptor: MutableMap = mutableMapOf() +) : Decoder, CompositeDecoder { + val id = nextEncoderId.incrementAndGet() + + override val serializersModule = EmptySerializersModule() + + private var nextElementIndex = 0 + private val elements = + when (value) { + null -> null + is Map<*, *> -> value.entries.toList() + is Collection<*> -> value.toList() + else -> null + } + + private fun log(message: String) { + println("zzyzx LoggingDecoder[$id] $message") + } + + private fun idFor(descriptor: SerialDescriptor) = + idBySerialDescriptor[descriptor] + ?: nextSerialDescriptorId.incrementAndGet().also { idBySerialDescriptor[descriptor] = it } + + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + log( + "beginStructure() descriptorId=${idFor(descriptor)} kind=${descriptor.kind} " + + "elementsCount=${descriptor.elementsCount}" + ) + return LoggingDecoder(idBySerialDescriptor) + } + + override fun endStructure(descriptor: SerialDescriptor) { + log("endStructure() descriptorId=${idFor(descriptor)} kind=${descriptor.kind}") + } + + override fun decodeBoolean(): Boolean { + log("decodeBoolean() returns true") + return true + } + + override fun decodeByte(): Byte { + log("decodeByte() returns 111") + return 111 + } + + override fun decodeChar(): Char { + log("decodeChar() returns Z") + return 'Z' + } + + override fun decodeDouble(): Double { + log("decodeDouble() returns 123.45") + return 123.45 + } + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { + log("decodeEnum() returns 0") + return 0 + } + + override fun decodeFloat(): Float { + log("decodeFloat() returns 678.90") + return 678.90f + } + + override fun decodeInline(descriptor: SerialDescriptor): Decoder { + log("decodeInline() kind=${descriptor.kind} serialName=${descriptor.serialName}") + return LoggingDecoder(idBySerialDescriptor) + } + + override fun decodeInt(): Int { + log("decodeInt() returns 4242") + return 4242 + } + + override fun decodeLong(): Long { + log("decodeLong() returns 987654") + return 987654 + } + + @ExperimentalSerializationApi + override fun decodeNotNullMark(): Boolean { + log("decodeNotNullMark() returns false") + return false + } + + @ExperimentalSerializationApi + override fun decodeNull(): Nothing? { + log("decodeNull() returns null") + return null + } + + override fun decodeShort(): Short { + log("decodeShort() returns 554433") + return 554433.toShort() + } + + override fun decodeString(): String { + log("decodeString() returns \"Hello World\"") + return "Hello World" + } + + override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int): Boolean { + log( + "decodeBooleanElement() index=$index elementName=${descriptor.getElementName(index)}" + + " returns false" + ) + return false + } + + override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte { + log( + "decodeByteElement() index=$index elementName=${descriptor.getElementName(index)}" + + " returns 66" + ) + return 66 + } + + override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char { + log( + "decodeCharElement() index=$index elementName=${descriptor.getElementName(index)}" + + " returns X" + ) + return 'X' + } + + override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double { + log( + "decodeDoubleElement() index=$index elementName=${descriptor.getElementName(index)}" + + " returns 543.21" + ) + return 543.21 + } + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + log("zzyzx elements=$elements") + if (elements === null || nextElementIndex >= elements.size) { + log("decodeElementIndex() returns DECODE_DONE") + return CompositeDecoder.DECODE_DONE + } + val elementIndex = nextElementIndex++ + log("decodeElementIndex() returns $elementIndex") + return elementIndex + } + + override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float { + log( + "decodeFloatElement() index=$index elementName=${descriptor.getElementName(index)}" + + "returns 987.65" + ) + return 987.65f + } + + override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder { + log("decodeInlineElement() index=$index elementName=${descriptor.getElementName(index)}") + return LoggingDecoder(idBySerialDescriptor) + } + + override fun decodeIntElement(descriptor: SerialDescriptor, index: Int): Int { + log( + "decodeIntElement() index=$index elementName=${descriptor.getElementName(index)}" + + " returns 5555" + ) + return 5555 + } + + override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long { + log( + "decodeLongElement() index=$index elementName=${descriptor.getElementName(index)}" + + " returns 848484848" + ) + return 848484848 + } + + override fun decodeShortElement(descriptor: SerialDescriptor, index: Int): Short { + log( + "decodeShortElement() index=$index elementName=${descriptor.getElementName(index)}" + + " returns 443344" + ) + return 443344.toShort() + } + + override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String { + log( + "decodeStringElement() index=$index elementName=${descriptor.getElementName(index)}" + + " returns \"Goodbye Cruel World\"" + ) + return "Goodbye Cruel World" + } + + @ExperimentalSerializationApi + override fun decodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T? { + log( + "decodeNullableSerializableElement()" + + "index=$index elementName=${descriptor.getElementName(index)}" + + " previousValue=$previousValue" + + " returns null" + ) + return null + } + + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T { + log( + "decodeSerializableElement()" + + "index=$index elementName=${descriptor.getElementName(index)}" + + " previousValue=$previousValue" + ) + return decodeSerializableValue(deserializer) + } + + companion object { + + fun decode(serializer: DeserializationStrategy, value: Map<*, *>): T = + LoggingDecoder(value).decodeSerializableValue(serializer) + + inline fun decode(value: Map<*, *>): T = decode(serializer(), value) + + private val nextEncoderId = AtomicLong(0) + private val nextSerialDescriptorId = AtomicLong(998800000L) + } +} From c7561766da26c6e22a7135699b5b779f7d64b2cd Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 17 Nov 2023 03:01:35 -0500 Subject: [PATCH 063/573] Implement serialization decoder (#514) --- .../dataconnect/FirebaseDataConnectTest.kt | 87 +--- .../dataconnect/QuerySubscriptionTest.kt | 16 +- .../dataconnect/generated/PostsTest.kt | 2 +- .../dataconnect/testutil/IdentityCodec.kt | 7 - .../testutil/schemas/PersonSchema.kt | 79 +-- .../testutil/schemas/PersonSchemaTest.kt | 46 +- .../google/firebase/dataconnect/BaseRef.kt | 7 +- .../dataconnect/DataConnectGrpcClient.kt | 43 +- .../dataconnect/FirebaseDataConnect.kt | 123 +++-- .../firebase/dataconnect/MutationRef.kt | 7 +- .../dataconnect/ProtoStructDecoder.kt | 321 +++++++++++- .../dataconnect/ProtoStructEncoder.kt | 3 +- .../google/firebase/dataconnect/QueryRef.kt | 5 +- .../apiproposal/QueryApiProposal.kt | 41 +- .../generated/CreatePostMutation.kt | 13 +- .../dataconnect/generated/GetPostQuery.kt | 84 +-- .../dataconnect/ProtoStructDecoderTest.kt | 482 +++++++++++------- 17 files changed, 803 insertions(+), 563 deletions(-) delete mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/IdentityCodec.kt diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index e393a1e6855..a96a18210b6 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -21,7 +21,6 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule -import com.google.firebase.dataconnect.testutil.IdentityCodec import com.google.firebase.dataconnect.testutil.TestDataConnectFactory import com.google.firebase.dataconnect.testutil.TestFirebaseAppFactory import com.google.firebase.dataconnect.testutil.installEmulatorSchema @@ -33,7 +32,6 @@ import kotlin.concurrent.withLock import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlinx.serialization.Serializable -import kotlinx.serialization.serializer import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test @@ -313,37 +311,32 @@ class FirebaseDataConnectTest { @Serializable data class CreatePostVariables(val data: PostData) @Serializable data class GetPostVariables(val id: String) + @Serializable data class GetPostResultPost(val content: String, val comments: List) + @Serializable data class GetPostResult(val post: GetPostResultPost) + run { val mutation = - dc.mutation>( + dc.mutation( operationName = "createPost", operationSet = "crud", revision = "TestRevision", - codec = IdentityCodec, - variablesSerializer = serializer(), ) val mutationResponse = mutation.execute(CreatePostVariables(PostData(id = postId, content = postContent))) - assertWithMessage("mutationResponse") - .that(mutationResponse) - .containsExactlyEntriesIn(mapOf("post_insert" to null)) + assertWithMessage("mutationResponse").that(mutationResponse).isSameInstanceAs(Unit) } run { val query = - dc.query>( + dc.query( operationName = "getPost", operationSet = "crud", revision = "TestRevision", - codec = IdentityCodec, - variablesSerializer = serializer(), ) val queryResult = query.execute(GetPostVariables(id = postId)) assertWithMessage("queryResponse") .that(queryResult) - .containsExactlyEntriesIn( - mapOf("post" to mapOf("content" to postContent, "comments" to emptyList())) - ) + .isEqualTo(GetPostResult(GetPostResultPost(content = postContent, comments = emptyList()))) } } @@ -352,65 +345,36 @@ class FirebaseDataConnectTest { @Serializable data class PersonData(val id: String, val name: String, val age: Int? = null) @Serializable data class CreatePersonVariables(val data: PersonData) @Serializable data class GetPersonVariables(val id: String) + @Serializable data class GetPersonResultPerson(val name: String, val age: Int?) + @Serializable data class GetPersonResult(val person: GetPersonResultPerson) + @Serializable + data class GetAllPeopleResultPerson(val id: String, val name: String, val age: Int?) + @Serializable data class GetAllPeopleResult(val people: List) suspend fun FirebaseDataConnect.createPerson(id: String, name: String, age: Int? = null) = - mutation>( + mutation( operationName = "createPerson", operationSet = "ops", revision = "42", - codec = IdentityCodec, - variablesSerializer = serializer(), ) .execute(CreatePersonVariables(PersonData(id = id, name = name, age = age))) suspend fun FirebaseDataConnect.getPerson(id: String) = - query>( + query( operationName = "getPerson", operationSet = "ops", revision = "42", - codec = IdentityCodec, - variablesSerializer = serializer(), ) .execute(GetPersonVariables(id = id)) suspend fun FirebaseDataConnect.getAllPeople() = - query>( + query( operationName = "getAllPeople", operationSet = "ops", revision = "42", - codec = IdentityCodec, - variablesSerializer = serializer(), ) .execute(Unit) - fun Map<*, *>.assertEqualsGetPersonResponse(name: String, age: Double?) { - assertThat(keys).containsExactly("person") - get("person").let { personMap -> - assertThat(personMap).isInstanceOf(Map::class.java) - (personMap as Map<*, *>).let { assertThat(it).containsExactly("name", name, "age", age) } - } - } - - data class IdNameAgeTuple(val id: Any?, val name: Any?, val age: Any?) - - fun Map<*, *>.assertEqualsGetPeopleResponse(vararg entries: IdNameAgeTuple) { - assertThat(keys).containsExactly("people") - get("people").let { peopleList -> - assertThat(peopleList).isInstanceOf(List::class.java) - val actualPeople = - (peopleList as Iterable<*>).mapIndexed { index, entry -> - assertWithMessage("people[$index]").that(entry).isInstanceOf(Map::class.java) - (entry as Map<*, *>).let { personMap -> - assertWithMessage("people[$index].keys") - .that(personMap.keys) - .containsExactly("id", "name", "age") - IdNameAgeTuple(id = personMap["id"], name = personMap["name"], age = personMap["age"]) - } - } - assertThat(actualPeople).containsExactlyElementsIn(entries) - } - } - val dataConnect = dataConnectFactory.newInstance() runBlocking { @@ -419,18 +383,15 @@ class FirebaseDataConnectTest { dataConnect.createPerson(id = "TestId1", name = "TestName1") dataConnect.createPerson(id = "TestId2", name = "TestName2", age = 999) - dataConnect - .getPerson(id = "TestId1") - .assertEqualsGetPersonResponse(name = "TestName1", age = null) - dataConnect - .getPerson(id = "TestId2") - .assertEqualsGetPersonResponse(name = "TestName2", age = 999.0) - - dataConnect - .getAllPeople() - .assertEqualsGetPeopleResponse( - IdNameAgeTuple(id = "TestId1", name = "TestName1", age = null), - IdNameAgeTuple(id = "TestId2", name = "TestName2", age = 999.0), + assertThat(dataConnect.getPerson(id = "TestId1")) + .isEqualTo(GetPersonResult(GetPersonResultPerson(name = "TestName1", age = null))) + assertThat(dataConnect.getPerson(id = "TestId2")) + .isEqualTo(GetPersonResult(GetPersonResultPerson(name = "TestName2", age = 999))) + + assertThat(dataConnect.getAllPeople().people) + .containsExactly( + GetAllPeopleResultPerson(id = "TestId1", name = "TestName1", age = null), + GetAllPeopleResultPerson(id = "TestId2", name = "TestName2", age = 999), ) } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt index 931723595f0..4bb78c104b9 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt @@ -58,7 +58,7 @@ class QuerySubscriptionTest { } @Test - fun lastResult_should_be_equal_to_the_last_collected_result() = runBlocking { + fun lastResult_should_be_equal_to_the_last_collected_result(): Unit = runBlocking { schema.createPerson.execute(id = "TestId", name = "TestPerson", age = 42) val querySubscription = schema.getPerson.subscribe(id = "42") val result = querySubscription.flow.timeout(2000.seconds).first() @@ -66,7 +66,7 @@ class QuerySubscriptionTest { } @Test - fun reload_should_notify_collecting_flows() = runBlocking { + fun reload_should_notify_collecting_flows(): Unit = runBlocking { schema.createPerson.execute(id = "TestId12345", name = "Name0", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") @@ -91,7 +91,7 @@ class QuerySubscriptionTest { } @Test - fun flow_collect_should_get_immediately_invoked_with_last_result() = runBlocking { + fun flow_collect_should_get_immediately_invoked_with_last_result(): Unit = runBlocking { schema.createPerson.execute(id = "TestId12345", name = "TestName", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") @@ -107,7 +107,7 @@ class QuerySubscriptionTest { } @Test - fun slow_flows_do_not_block_fast_flows() = runBlocking { + fun slow_flows_do_not_block_fast_flows(): Unit = runBlocking { schema.createPerson.execute(id = "TestId12345", name = "TestName", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") @@ -127,7 +127,7 @@ class QuerySubscriptionTest { } @Test - fun reload_delivers_result_to_all_registered_flows() = runBlocking { + fun reload_delivers_result_to_all_registered_flows(): Unit = runBlocking { schema.createPerson.execute(id = "TestId12345", name = "TestName0", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") @@ -196,7 +196,11 @@ class QuerySubscriptionTest { } private fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = - isEqualTo(PersonSchema.GetPersonQuery.Result(name = name, age = age)) + isEqualTo( + PersonSchema.GetPersonQuery.Result( + PersonSchema.GetPersonQuery.Result.Person(name = name, age = age) + ) + ) private suspend fun ReceiveChannel.purge(timeout: Duration): List = coroutineScope { mutableListOf() diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt index bf2f94f5ff0..fbcf1dac96c 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt @@ -45,7 +45,7 @@ class PostsTest { val dc = dataConnectFactory.newInstance(service = "local") runBlocking { val queryResponse = dc.queries.getPost.execute(id = UUID.randomUUID().toString()) - assertWithMessage("queryResponse").that(queryResponse).isNull() + assertWithMessage("queryResponse").that(queryResponse.post).isNull() } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/IdentityCodec.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/IdentityCodec.kt deleted file mode 100644 index 726221c0b9e..00000000000 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/IdentityCodec.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.google.firebase.dataconnect.testutil - -import com.google.firebase.dataconnect.BaseRef - -object IdentityCodec : BaseRef.Codec> { - override fun decodeResult(map: Map): Map = map -} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index 86f0cf1bad9..fc8be52cadb 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -14,13 +14,13 @@ package com.google.firebase.dataconnect.testutil.schemas -import com.google.firebase.dataconnect.BaseRef import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.MutationRef import com.google.firebase.dataconnect.QueryRef +import com.google.firebase.dataconnect.mutation +import com.google.firebase.dataconnect.query import com.google.firebase.dataconnect.testutil.installEmulatorSchema import kotlinx.serialization.Serializable -import kotlinx.serialization.serializer class PersonSchema(val dataConnect: FirebaseDataConnect) { @@ -29,114 +29,68 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { } val createPerson = - dataConnect.mutation( + dataConnect.mutation( operationName = "createPerson", operationSet = "ops", revision = "42", - codec = CreatePersonMutation, - variablesSerializer = serializer(), ) class CreatePersonMutation private constructor() { - @Serializable data class PersonData(val id: String, val name: String, val age: Int? = null) @Serializable data class Variables(val data: PersonData) - - internal companion object Codec : BaseRef.Codec { - override fun decodeResult(map: Map) {} - } } val updatePerson = - dataConnect.mutation( + dataConnect.mutation( operationName = "updatePerson", operationSet = "ops", revision = "42", - codec = UpdatePersonMutation, - variablesSerializer = serializer(), ) class UpdatePersonMutation private constructor() { - @Serializable data class PersonData(val name: String? = null, val age: Int? = null) @Serializable data class Variables(val id: String, val data: PersonData) - - internal companion object Codec : BaseRef.Codec { - override fun decodeResult(map: Map) {} - } } val deletePerson = - dataConnect.mutation( + dataConnect.mutation( operationName = "deletePerson", operationSet = "ops", revision = "42", - codec = DeletePersonMutation, - variablesSerializer = serializer(), ) class DeletePersonMutation private constructor() { - @Serializable data class Variables(val id: String) - - internal companion object Codec : BaseRef.Codec { - override fun decodeResult(map: Map) {} - } } val getPerson = - dataConnect.query( + dataConnect.query( operationName = "getPerson", operationSet = "ops", revision = "42", - codec = GetPersonQuery, - variablesSerializer = serializer(), ) class GetPersonQuery private constructor() { - @Serializable data class Variables(val id: String, val name: String? = null, val age: Int? = null) - data class Result(val name: String, val age: Int? = null) - internal companion object Codec : BaseRef.Codec { - override fun decodeResult(map: Map) = - (map["person"] as Map<*, *>?)?.let { - Result(name = it["name"] as String, age = (it["age"] as Double?)?.toInt()) - } + @Serializable + data class Result(val person: Person?) { + @Serializable data class Person(val name: String, val age: Int? = null) } } val getAllPeople = - dataConnect.query( + dataConnect.query( operationName = "getAllPeople", operationSet = "ops", revision = "42", - codec = GetAllPeopleQuery, - variablesSerializer = serializer(), ) class GetAllPeopleQuery private constructor() { - - data class Person(val id: String, val name: String, val age: Int?) - data class Result(val people: List) - - internal companion object Codec : BaseRef.Codec { - override fun decodeResult(map: Map) = - (map["people"] as List<*>).let { people -> - Result( - people = - people - .map { it as Map<*, *> } - .map { - Person( - id = it["id"] as String, - name = it["name"] as String, - age = (it["age"] as Double?)?.toInt() - ) - } - ) - } + @Serializable + data class Result(val people: List) { + @Serializable data class Person(val id: String, val name: String, val age: Int?) } } } @@ -174,11 +128,12 @@ object DeletePersonMutationExt { } object GetPersonQueryExt { - suspend fun QueryRef + suspend fun QueryRef .execute(id: String) = execute(PersonSchema.GetPersonQuery.Variables(id = id)) - fun QueryRef - .subscribe(id: String) = subscribe(PersonSchema.GetPersonQuery.Variables(id = id)) + fun QueryRef.subscribe( + id: String + ) = subscribe(PersonSchema.GetPersonQuery.Variables(id = id)) } object GetAllPeoplePersonQueryExt { diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt index c520ed72e6b..41fcab8e17c 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt @@ -33,20 +33,20 @@ class PersonSchemaTest { fun createPersonShouldCreateTheSpecifiedPerson(): Unit = runBlocking { schema.createPerson.execute(id = "1234", name = "TestName", age = 42) - val person = schema.getPerson.execute(id = "1234") + val result = schema.getPerson.execute(id = "1234") - assertThat(person?.name).isEqualTo("TestName") - assertThat(person?.age).isEqualTo(42) + assertThat(result.person?.name).isEqualTo("TestName") + assertThat(result.person?.age).isEqualTo(42) } @Test fun deletePersonShouldDeleteTheSpecifiedPerson(): Unit = runBlocking { schema.createPerson.execute(id = "1234", name = "TestName", age = 42) - assertThat(schema.getPerson.execute(id = "1234")).isNotNull() + assertThat(schema.getPerson.execute(id = "1234").person).isNotNull() schema.deletePerson.execute(id = "1234") - assertThat(schema.getPerson.execute(id = "1234")).isNull() + assertThat(schema.getPerson.execute(id = "1234").person).isNull() } @Test @@ -55,9 +55,9 @@ class PersonSchemaTest { schema.updatePerson.execute(id = "1234", name = "TestName99", age = 999) - val person = schema.getPerson.execute(id = "1234") - assertThat(person?.name).isEqualTo("TestName99") - assertThat(person?.age).isEqualTo(999) + val result = schema.getPerson.execute(id = "1234") + assertThat(result.person?.name).isEqualTo("TestName99") + assertThat(result.person?.age).isEqualTo(999) } @Test @@ -66,25 +66,25 @@ class PersonSchemaTest { schema.createPerson.execute(id = "222", name = "Name222", age = 222) schema.createPerson.execute(id = "333", name = "Name333", age = null) - val person1 = schema.getPerson.execute(id = "111") - val person2 = schema.getPerson.execute(id = "222") - val person3 = schema.getPerson.execute(id = "333") + val result1 = schema.getPerson.execute(id = "111") + val result2 = schema.getPerson.execute(id = "222") + val result3 = schema.getPerson.execute(id = "333") - assertThat(person1?.name).isEqualTo("Name111") - assertThat(person1?.age).isEqualTo(111) - assertThat(person2?.name).isEqualTo("Name222") - assertThat(person2?.age).isEqualTo(222) - assertThat(person3?.name).isEqualTo("Name333") - assertThat(person3?.age).isNull() + assertThat(result1.person?.name).isEqualTo("Name111") + assertThat(result1.person?.age).isEqualTo(111) + assertThat(result2.person?.name).isEqualTo("Name222") + assertThat(result2.person?.age).isEqualTo(222) + assertThat(result3.person?.name).isEqualTo("Name333") + assertThat(result3.person?.age).isNull() } @Test - fun getPersonShouldReturnNullIfThePersonDoesNotExist(): Unit = runBlocking { + fun getPersonShouldReturnNullPersonIfThePersonDoesNotExist(): Unit = runBlocking { schema.createPerson.execute(id = "111", name = "Name111", age = 111) - val person = schema.getPerson.execute(id = "IdOfPersonThatDoesNotExit") + val result = schema.getPerson.execute(id = "IdOfPersonThatDoesNotExit") - assertThat(person).isNull() + assertThat(result.person).isNull() } @Test @@ -102,9 +102,9 @@ class PersonSchemaTest { assertThat(result.people) .containsExactly( - PersonSchema.GetAllPeopleQuery.Person(id = "111", name = "Name111", age = 111), - PersonSchema.GetAllPeopleQuery.Person(id = "222", name = "Name222", age = 222), - PersonSchema.GetAllPeopleQuery.Person(id = "333", name = "Name333", age = null), + PersonSchema.GetAllPeopleQuery.Result.Person(id = "111", name = "Name111", age = 111), + PersonSchema.GetAllPeopleQuery.Result.Person(id = "222", name = "Name222", age = 222), + PersonSchema.GetAllPeopleQuery.Result.Person(id = "333", name = "Name333", age = null), ) } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt index 755a08dc2a7..eebd4c5a04b 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt @@ -13,6 +13,7 @@ // limitations under the License. package com.google.firebase.dataconnect +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy abstract class BaseRef @@ -21,12 +22,8 @@ internal constructor( internal val operationName: String, internal val operationSet: String, internal val revision: String, - internal val codec: Codec, internal val variablesSerializer: SerializationStrategy, + internal val resultDeserializer: DeserializationStrategy, ) { abstract suspend fun execute(variables: VariablesType): ResultType - - interface Codec { - fun decodeResult(map: Map): ResultType - } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 11ecb3d1a34..de38b213b83 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -15,8 +15,6 @@ package com.google.firebase.dataconnect import android.content.Context import com.google.android.gms.security.ProviderInstaller -import com.google.protobuf.Struct -import com.google.protobuf.Value import google.internal.firebase.firemat.v0.DataServiceGrpcKt.DataServiceCoroutineStub import google.internal.firebase.firemat.v0.executeMutationRequest import google.internal.firebase.firemat.v0.executeQueryRequest @@ -25,6 +23,7 @@ import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder import java.util.concurrent.Executor import java.util.concurrent.TimeUnit +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy internal class DataConnectGrpcClient( @@ -79,13 +78,14 @@ internal class DataConnectGrpcClient( private val grpcStub: DataServiceCoroutineStub by lazy { DataServiceCoroutineStub(grpcChannel) } - suspend fun executeQuery( + suspend fun executeQuery( operationSet: String, operationName: String, revision: String, - variables: V, - variablesSerializer: SerializationStrategy - ): Map { + variables: VariablesType, + variablesSerializer: SerializationStrategy, + resultDeserializer: DeserializationStrategy + ): ResultType { val request = executeQueryRequest { this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName @@ -107,16 +107,18 @@ internal class DataConnectGrpcClient( response.errorsList.map { it.toString() } ) } - return mapFromStruct(response.data) + + return decodeFromStruct(resultDeserializer, response.data) } - suspend fun executeMutation( + suspend fun executeMutation( operationSet: String, operationName: String, revision: String, - variables: V, - variablesSerializer: SerializationStrategy - ): Map { + variables: VariablesType, + variablesSerializer: SerializationStrategy, + resultDeserializer: DeserializationStrategy + ): ResultType { val request = executeMutationRequest { this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName @@ -138,7 +140,8 @@ internal class DataConnectGrpcClient( response.errorsList.map { it.toString() } ) } - return mapFromStruct(response.data) + + return decodeFromStruct(resultDeserializer, response.data) } override fun toString(): String { @@ -157,19 +160,3 @@ internal class DataConnectGrpcClient( "projects/$projectId/locations/$location/services/$service/" + "operationSets/$operationSet/revisions/$revision" } - -private fun mapFromStruct(struct: Struct): Map = - struct.fieldsMap.mapValues { objectFromStructValue(it.value) } - -private fun objectFromStructValue(struct: Value): Any? = - struct.run { - when (kindCase) { - Value.KindCase.NULL_VALUE -> null - Value.KindCase.BOOL_VALUE -> boolValue - Value.KindCase.NUMBER_VALUE -> numberValue - Value.KindCase.STRING_VALUE -> stringValue - Value.KindCase.LIST_VALUE -> listValue.valuesList.map { objectFromStructValue(it) } - Value.KindCase.STRUCT_VALUE -> mapFromStruct(structValue) - else -> throw ResultDecodeException("unsupported Struct kind: $kindCase") - } - } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 376b97f4139..192b0feb666 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -27,7 +27,9 @@ import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.serializer class FirebaseDataConnect internal constructor( @@ -93,63 +95,28 @@ internal constructor( internal suspend fun executeQuery(ref: QueryRef, variables: V): R = withContext(sequentialDispatcher) { grpcClient } .run { - ref.codec.decodeResult( - executeQuery( - operationName = ref.operationName, - operationSet = ref.operationSet, - revision = ref.revision, - variables = variables, - variablesSerializer = ref.variablesSerializer, - ) + executeQuery( + operationName = ref.operationName, + operationSet = ref.operationSet, + revision = ref.revision, + variables = variables, + variablesSerializer = ref.variablesSerializer, + resultDeserializer = ref.resultDeserializer ) } internal suspend fun executeMutation(ref: MutationRef, variables: V): R = withContext(sequentialDispatcher) { grpcClient } .run { - ref.codec.decodeResult( - executeMutation( - operationName = ref.operationName, - operationSet = ref.operationSet, - revision = ref.revision, - variables = variables, - variablesSerializer = ref.variablesSerializer, - ) + executeMutation( + operationName = ref.operationName, + operationSet = ref.operationSet, + revision = ref.revision, + variables = variables, + variablesSerializer = ref.variablesSerializer, + resultDeserializer = ref.resultDeserializer ) } - - fun query( - operationName: String, - operationSet: String, - revision: String, - codec: BaseRef.Codec, - variablesSerializer: SerializationStrategy, - ): QueryRef = - QueryRef( - dataConnect = this, - operationName = operationName, - operationSet = operationSet, - revision = revision, - codec = codec, - variablesSerializer = variablesSerializer, - ) - - fun mutation( - operationName: String, - operationSet: String, - revision: String, - codec: BaseRef.Codec, - variablesSerializer: SerializationStrategy, - ): MutationRef = - MutationRef( - dataConnect = this, - operationName = operationName, - operationSet = operationSet, - revision = revision, - codec = codec, - variablesSerializer = variablesSerializer, - ) - override fun close() { logger.debug { "close() called" } runBlocking(sequentialDispatcher) { @@ -215,6 +182,64 @@ internal constructor( } } +inline fun FirebaseDataConnect.query( + operationName: String, + operationSet: String, + revision: String +): QueryRef = + query( + operationName = operationName, + operationSet = operationSet, + revision = revision, + variablesSerializer = serializer(), + resultDeserializer = serializer(), + ) + +fun FirebaseDataConnect.query( + operationName: String, + operationSet: String, + revision: String, + variablesSerializer: SerializationStrategy, + resultDeserializer: DeserializationStrategy +): QueryRef = + QueryRef( + dataConnect = this, + operationName = operationName, + operationSet = operationSet, + revision = revision, + variablesSerializer = variablesSerializer, + resultDeserializer = resultDeserializer + ) + +inline fun FirebaseDataConnect.mutation( + operationName: String, + operationSet: String, + revision: String, +): MutationRef = + mutation( + operationName = operationName, + operationSet = operationSet, + revision = revision, + variablesSerializer = serializer(), + resultDeserializer = serializer(), + ) + +fun FirebaseDataConnect.mutation( + operationName: String, + operationSet: String, + revision: String, + variablesSerializer: SerializationStrategy, + resultDeserializer: DeserializationStrategy +): MutationRef = + MutationRef( + dataConnect = this, + operationName = operationName, + operationSet = operationSet, + revision = revision, + variablesSerializer = variablesSerializer, + resultDeserializer = resultDeserializer + ) + open class DataConnectException internal constructor(message: String, cause: Throwable? = null) : Exception(message, cause) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt index 053eecefc10..b06ab701de2 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt @@ -13,6 +13,7 @@ // limitations under the License. package com.google.firebase.dataconnect +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy class MutationRef @@ -21,16 +22,16 @@ internal constructor( operationName: String, operationSet: String, revision: String, - codec: Codec, - variablesSerializer: SerializationStrategy + variablesSerializer: SerializationStrategy, + resultDeserializer: DeserializationStrategy ) : BaseRef( dataConnect = dataConnect, operationName = operationName, operationSet = operationSet, revision = revision, - codec = codec, variablesSerializer = variablesSerializer, + resultDeserializer = resultDeserializer, ) { override suspend fun execute(variables: VariablesType): ResultType = dataConnect.executeMutation(this, variables) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt index 800d2645e89..4496d644c37 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt @@ -2,10 +2,17 @@ package com.google.firebase.dataconnect +import com.google.protobuf.ListValue +import com.google.protobuf.NullValue import com.google.protobuf.Struct +import com.google.protobuf.Value +import com.google.protobuf.Value.KindCase import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.elementNames import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.modules.EmptySerializersModule @@ -13,20 +20,55 @@ import kotlinx.serialization.serializer inline fun decodeFromStruct(struct: Struct): T = decodeFromStruct(serializer(), struct) -fun decodeFromStruct(serializer: DeserializationStrategy, struct: Struct): T { - return ProtoValueDecoder(struct).decodeSerializableValue(serializer) +fun decodeFromStruct(deserializer: DeserializationStrategy, struct: Struct): T { + val protoValue = Value.newBuilder().setStructValue(struct).build() + return ProtoValueDecoder(protoValue, path = null).decodeSerializableValue(deserializer) } -private class ProtoValueDecoder(private val struct: Struct) : Decoder { +private fun Value.decode(path: String?, expectedKindCase: KindCase, block: (Value) -> T): T = + if (kindCase != expectedKindCase) { + throw SerializationException( + (if (path === null) "" else "decoding \"$path\" failed: ") + + "expected $expectedKindCase, but got $kindCase" + ) + } else { + block(this) + } + +private fun Value.decodeBoolean(path: String?): Boolean = + decode(path, KindCase.BOOL_VALUE) { it.boolValue } + +private fun Value.decodeDouble(path: String?): Double = + decode(path, KindCase.NUMBER_VALUE) { it.numberValue } + +private fun Value.decodeString(path: String?): String = + decode(path, KindCase.STRING_VALUE) { it.stringValue } + +private fun Value.decodeStruct(path: String?): Struct = + decode(path, KindCase.STRUCT_VALUE) { it.structValue } + +private fun Value.decodeList(path: String?): ListValue = + decode(path, KindCase.LIST_VALUE) { it.listValue } + +private fun Value.decodeNull(path: String?): NullValue = + decode(path, KindCase.NULL_VALUE) { it.nullValue } + +private fun Value.decodeInt(path: String?): Int = decodeDouble(path).toInt() + +private class ProtoValueDecoder(private val valueProto: Value, private val path: String?) : + Decoder { + override val serializersModule = EmptySerializersModule() - override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { - TODO("Not yet implemented") - } + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = + when (val kind = descriptor.kind) { + is StructureKind.CLASS -> ProtoStructValueDecoder(valueProto.decodeStruct(path), path) + is StructureKind.LIST -> ProtoListValueDecoder(valueProto.decodeList(path), path) + is StructureKind.OBJECT -> ProtoObjectValueDecoder + else -> throw IllegalArgumentException("unsupported SerialKind: ${kind::class.qualifiedName}") + } - override fun decodeBoolean(): Boolean { - TODO("Not yet implemented") - } + override fun decodeBoolean() = valueProto.decodeBoolean(path) override fun decodeByte(): Byte { TODO("Not yet implemented") @@ -36,9 +78,7 @@ private class ProtoValueDecoder(private val struct: Struct) : Decoder { TODO("Not yet implemented") } - override fun decodeDouble(): Double { - TODO("Not yet implemented") - } + override fun decodeDouble() = valueProto.decodeDouble(path) override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { TODO("Not yet implemented") @@ -52,9 +92,7 @@ private class ProtoValueDecoder(private val struct: Struct) : Decoder { TODO("Not yet implemented") } - override fun decodeInt(): Int { - TODO("Not yet implemented") - } + override fun decodeInt(): Int = valueProto.decodeInt(path) override fun decodeLong(): Long { TODO("Not yet implemented") @@ -62,19 +100,266 @@ private class ProtoValueDecoder(private val struct: Struct) : Decoder { @ExperimentalSerializationApi override fun decodeNotNullMark(): Boolean { - TODO("Not yet implemented") + return !valueProto.hasNullValue() } @ExperimentalSerializationApi override fun decodeNull(): Nothing? { - TODO("Not yet implemented") + valueProto.decodeNull(path) + return null } override fun decodeShort(): Short { TODO("Not yet implemented") } - override fun decodeString(): String { + override fun decodeString() = valueProto.decodeString(path) +} + +private class ProtoStructValueDecoder(private val struct: Struct, private val path: String?) : + CompositeDecoder { + override val serializersModule = EmptySerializersModule() + + override fun endStructure(descriptor: SerialDescriptor) {} + + private lateinit var elementIndexes: Iterator + + private fun computeElementIndexSet(descriptor: SerialDescriptor) = + buildSet { + addAll(struct.fieldsMap.keys) + addAll(descriptor.elementNames) + } + .map(descriptor::getElementIndex) + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + if (!::elementIndexes.isInitialized) { + elementIndexes = computeElementIndexSet(descriptor).sorted().iterator() + } + return if (elementIndexes.hasNext()) elementIndexes.next() else CompositeDecoder.DECODE_DONE + } + + override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(descriptor, index, Value::decodeBoolean) + + override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte { + TODO("Not yet implemented") + } + + override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char { + TODO("Not yet implemented") + } + + override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double = + decodeValueElement(descriptor, index, Value::decodeDouble) + + override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float { + TODO("Not yet implemented") + } + + override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder { + TODO("Not yet implemented") + } + + override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(descriptor, index, Value::decodeInt) + + override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long { + TODO("Not yet implemented") + } + + override fun decodeShortElement(descriptor: SerialDescriptor, index: Int): Short { + TODO("Not yet implemented") + } + + override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String = + decodeValueElement(descriptor, index, Value::decodeString) + + private fun decodeValueElement( + descriptor: SerialDescriptor, + index: Int, + block: Value.(String?) -> T + ): T { + val elementName = descriptor.getElementName(index) + val value = + struct.fieldsMap[elementName] + ?: throw SerializationException( + "element \"$elementName\" missing (expected ${descriptor.getElementDescriptor(index).kind})" + ) + return block(value, elementPathForName(elementName)) + } + + @ExperimentalSerializationApi + override fun decodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T? { + val elementName = descriptor.getElementName(index) + return if (previousValue !== null) { + previousValue + } else if (!struct.containsFields(elementName)) { + null + } else if (struct.getFieldsOrThrow(elementName).hasNullValue()) { + null + } else { + decodeSerializableElement(descriptor, index, deserializer, previousValue = null) + } + } + + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T { + if (previousValue !== null) { + return previousValue + } + + val elementName = descriptor.getElementName(index) + val valueProto = struct.fieldsMap[elementName] + if (valueProto === null) { + throw SerializationException( + "element \"$elementName\" missing; expected ${descriptor.getElementDescriptor(index).kind}" + ) + } + + return deserializer.deserialize(ProtoValueDecoder(valueProto, elementPathForName(elementName))) + } + + private fun elementPathForName(elementName: String) = + if (path === null) elementName else "${path}.${elementName}" +} + +private class ProtoListValueDecoder(private val list: ListValue, private val path: String?) : + CompositeDecoder { + override val serializersModule = EmptySerializersModule() + + override fun endStructure(descriptor: SerialDescriptor) {} + + private val elementIndexes: Iterator by lazy { + list.valuesList.mapIndexed { index, _ -> index }.iterator() + } + + override fun decodeElementIndex(descriptor: SerialDescriptor) = + if (elementIndexes.hasNext()) elementIndexes.next() else CompositeDecoder.DECODE_DONE + + override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeBoolean) + + override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte { + TODO("Not yet implemented") + } + + override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char { + TODO("Not yet implemented") + } + + override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double = + decodeValueElement(index, Value::decodeDouble) + + override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float { + TODO("Not yet implemented") + } + + override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder { + TODO("Not yet implemented") + } + + override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeInt) + + override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long { + TODO("Not yet implemented") + } + + override fun decodeShortElement(descriptor: SerialDescriptor, index: Int): Short { TODO("Not yet implemented") } + + override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String = + decodeValueElement(index, Value::decodeString) + + private inline fun decodeValueElement(index: Int, block: Value.(String?) -> T): T = + block(list.valuesList[index], elementPathForIndex(index)) + + @ExperimentalSerializationApi + override fun decodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T? { + return if (previousValue !== null) { + previousValue + } else if (list.valuesList[index].hasNullValue()) { + null + } else { + decodeSerializableElement(descriptor, index, deserializer, previousValue = null) + } + } + + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T = + if (previousValue !== null) previousValue + else + deserializer.deserialize( + ProtoValueDecoder(list.valuesList[index], elementPathForIndex(index)) + ) + + private fun elementPathForIndex(index: Int) = if (path === null) "[$index]" else "${path}[$index]" +} + +private object ProtoObjectValueDecoder : CompositeDecoder { + override val serializersModule = EmptySerializersModule() + + override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + @ExperimentalSerializationApi + override fun decodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ) = notSupported() + + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ) = notSupported() + + override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + private fun notSupported(): Nothing = + throw UnsupportedOperationException( + "The only valid method calls on ProtoObjectValueDecoder are " + + "decodeElementIndex() and endStructure()" + ) + + override fun decodeElementIndex(descriptor: SerialDescriptor) = CompositeDecoder.DECODE_DONE + + override fun endStructure(descriptor: SerialDescriptor) {} } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt index eb4f725c066..e020ab81d1a 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt @@ -253,8 +253,7 @@ private object ProtoObjectValueEncoder : CompositeEncoder { override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) = notSupported() - override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder = - notSupported() + override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int) = notSupported() override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) = notSupported() diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt index 3302268de44..6557587c71e 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt @@ -13,6 +13,7 @@ // limitations under the License. package com.google.firebase.dataconnect +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy class QueryRef @@ -21,16 +22,16 @@ internal constructor( operationName: String, operationSet: String, revision: String, - codec: Codec, variablesSerializer: SerializationStrategy, + resultDeserializer: DeserializationStrategy ) : BaseRef( dataConnect = dataConnect, operationName = operationName, operationSet = operationSet, revision = revision, - codec = codec, variablesSerializer = variablesSerializer, + resultDeserializer = resultDeserializer, ) { override suspend fun execute(variables: VariablesType): ResultType = dataConnect.executeQuery(this, variables) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index 1e77ab719b8..52e908b4a88 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationStrategy @@ -14,14 +15,6 @@ import kotlinx.serialization.SerializationStrategy class FirebaseDataConnect { - fun query( - operationName: String, - operationSet: String, - revision: String, - codec: BaseRef.Codec, - variablesSerializer: SerializationStrategy, - ): QueryRef = TODO() - class Queries internal constructor() { val dataConnect: FirebaseDataConnect get() = TODO() @@ -29,6 +22,20 @@ class FirebaseDataConnect { val queries: Queries = TODO() } +fun FirebaseDataConnect.query( + operationName: String, + operationSet: String, + revision: String, + variablesSerializer: SerializationStrategy, + resultDeserializer: DeserializationStrategy +): QueryRef = TODO() + +inline fun FirebaseDataConnect.query( + operationName: String, + operationSet: String, + revision: String +): QueryRef = TODO() + open class DataConnectException internal constructor() : Exception() open class NetworkTransportException internal constructor() : DataConnectException() @@ -94,31 +101,31 @@ class GetPostQuery private constructor() { @Serializable data class Variables(val id: String) - data class Result(val post: Post) { + data class Result(val post: Post?) { data class Post(val content: String, val comments: List) { data class Comment(val id: String, val content: String) } } companion object { - fun query(dataConnect: FirebaseDataConnect): QueryRef = TODO() + fun query(dataConnect: FirebaseDataConnect): QueryRef = TODO() } } -val FirebaseDataConnect.Queries.getPost: QueryRef +val FirebaseDataConnect.Queries.getPost: QueryRef get() = TODO() -suspend fun QueryRef.execute( +suspend fun QueryRef.execute( id: String -): GetPostQuery.Result? = TODO() +): GetPostQuery.Result = TODO() -fun QueryRef.subscribe( +fun QueryRef.subscribe( id: String -): QuerySubscription = TODO() +): QuerySubscription = TODO() -typealias GetPostQueryRef = QueryRef +typealias GetPostQueryRef = QueryRef -typealias GetPostQuerySubscription = QuerySubscription +typealias GetPostQuerySubscription = QuerySubscription //////////////////////////////////////////////////////////////////////////////////////////////////// // CUSTOMER CODE diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt index 66cb538fe6f..eec1fb18635 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt @@ -13,11 +13,10 @@ // limitations under the License. package com.google.firebase.dataconnect.generated -import com.google.firebase.dataconnect.BaseRef import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.MutationRef +import com.google.firebase.dataconnect.mutation import kotlinx.serialization.Serializable -import kotlinx.serialization.serializer class CreatePostMutation private constructor() { @@ -57,20 +56,12 @@ class CreatePostMutation private constructor() { } companion object { - fun mutation(dataConnect: FirebaseDataConnect) = - dataConnect.mutation( + dataConnect.mutation( operationName = "createPost", operationSet = "crud", revision = "1234567890abcdef", - codec = codec, - variablesSerializer = serializer() ) - - private val codec = - object : BaseRef.Codec { - override fun decodeResult(map: Map) {} - } } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt index 4332d127ea4..1401a4e5794 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -13,13 +13,11 @@ // limitations under the License. package com.google.firebase.dataconnect.generated -import com.google.firebase.dataconnect.BaseRef import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.QueryRef import com.google.firebase.dataconnect.QuerySubscription -import com.google.firebase.dataconnect.ResultDecodeException +import com.google.firebase.dataconnect.query import kotlinx.serialization.Serializable -import kotlinx.serialization.serializer class GetPostQuery private constructor() { @@ -39,99 +37,37 @@ class GetPostQuery private constructor() { } } - data class Result(val post: Post) { + @Serializable + data class Result(val post: Post?) { + @Serializable data class Post(val content: String, val comments: List) { - data class Comment(val id: String, val content: String) + @Serializable data class Comment(val id: String, val content: String) } } companion object { fun query(dataConnect: FirebaseDataConnect) = - dataConnect.query( + dataConnect.query( operationName = "getPost", operationSet = "crud", revision = "1234567890abcdef", - codec = codec, - variablesSerializer = serializer(), ) - - private val codec = - object : BaseRef.Codec { - override fun decodeResult(map: Map) = - map["post"].let { - if (it == null) { - null - } else { - Result( - _decodePost( - data = it as? Map<*, *> ?: decodeError("post", it, "Map", "result", map), - path = "post" - ) - ) - } - } - - private fun _decodePost(data: Map<*, *>, path: String) = - Result.Post( - content = - data["content"].let { - it as? String ?: decodeError("$path.content", it, "String", path, data) - }, - comments = - data["comments"].let { - _decodeComments( - it as? List<*> ?: decodeError("$path.comments", it, "List", path, data), - "$path.comments" - ) - } - ) - - private fun _decodeComments(data: List<*>, path: String) = - data.mapIndexed { index, it -> - _decodeComment( - it as? Map<*, *> ?: decodeError("$path[$index]", it, "Map", path, data), - "$path[$index]" - ) - } - - private fun _decodeComment(data: Map<*, *>, path: String) = - Result.Post.Comment( - id = - data["id"].let { it as? String ?: decodeError("$path.id", it, "String", path, data) }, - content = - data["content"].let { - it as? String ?: decodeError("$path.content", it, "String", path, data) - } - ) - - private fun decodeError( - path: String, - actual: Any?, - expected: String, - contextName: String, - context: Any? - ): Nothing = - throw ResultDecodeException( - "parsing GetPostQuery.Result failed: \"$path\" was expected to be $expected, " + - "but got $actual ($contextName=$context)" - ) - } } } -typealias GetPostQuerySubscription = QuerySubscription +typealias GetPostQuerySubscription = QuerySubscription val FirebaseDataConnect.Queries.getPost get() = GetPostQuery.query(dataConnect) -suspend fun QueryRef.execute(id: String) = +suspend fun QueryRef.execute(id: String) = execute(variablesFor(id = id)) -fun QueryRef.subscribe(id: String) = +fun QueryRef.subscribe(id: String) = subscribe(variablesFor(id = id)) -fun QuerySubscription.update( +fun QuerySubscription.update( block: GetPostQuery.Variables.Builder.() -> Unit ) = update(variables.build(block)) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt index 402d11173c8..ab41057119a 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt @@ -17,264 +17,362 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat -import java.util.concurrent.atomic.AtomicLong -import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.serializer import org.junit.Test class ProtoStructDecoderTest { @Test - fun `decodeFromStruct() can decode a Struct with a single String value`() { - @Serializable data class TestData(val value: String) - val struct = encodeToStruct(TestData(value = "Test Value")) + fun `decodeFromStruct() can decode a Struct with String values`() { + @Serializable data class TestData(val value1: String, val value2: String) + val struct = encodeToStruct(TestData(value1 = "foo", value2 = "bar")) val decodedTestData = decodeFromStruct(struct) - assertThat(decodedTestData).isEqualTo(TestData(value = "Test Value")) + assertThat(decodedTestData).isEqualTo(TestData(value1 = "foo", value2 = "bar")) } -} -/** - * A decoder that can be useful during testing to simply print the method invocations in order to - * discover how a decoder should be implemented. - */ -private class LoggingDecoder( - value: Any?, - private val idBySerialDescriptor: MutableMap = mutableMapOf() -) : Decoder, CompositeDecoder { - val id = nextEncoderId.incrementAndGet() - - override val serializersModule = EmptySerializersModule() - - private var nextElementIndex = 0 - private val elements = - when (value) { - null -> null - is Map<*, *> -> value.entries.toList() - is Collection<*> -> value.toList() - else -> null - } - - private fun log(message: String) { - println("zzyzx LoggingDecoder[$id] $message") - } + @Test + fun `decodeFromStruct() can decode a Struct with _nullable_ String values`() { + @Serializable data class TestData(val isNull: String?, val isNotNull: String?) + val struct = encodeToStruct(TestData(isNull = null, isNotNull = "NotNull")) - private fun idFor(descriptor: SerialDescriptor) = - idBySerialDescriptor[descriptor] - ?: nextSerialDescriptorId.incrementAndGet().also { idBySerialDescriptor[descriptor] = it } + val decodedTestData = decodeFromStruct(struct) - override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { - log( - "beginStructure() descriptorId=${idFor(descriptor)} kind=${descriptor.kind} " + - "elementsCount=${descriptor.elementsCount}" - ) - return LoggingDecoder(idBySerialDescriptor) + assertThat(decodedTestData).isEqualTo(TestData(isNull = null, isNotNull = "NotNull")) } - override fun endStructure(descriptor: SerialDescriptor) { - log("endStructure() descriptorId=${idFor(descriptor)} kind=${descriptor.kind}") - } + @Test + fun `decodeFromStruct() can decode a Struct with Boolean values`() { + @Serializable data class TestData(val value1: Boolean, val value2: Boolean) + val struct = encodeToStruct(TestData(value1 = true, value2 = false)) - override fun decodeBoolean(): Boolean { - log("decodeBoolean() returns true") - return true - } + val decodedTestData = decodeFromStruct(struct) - override fun decodeByte(): Byte { - log("decodeByte() returns 111") - return 111 + assertThat(decodedTestData).isEqualTo(TestData(value1 = true, value2 = false)) } - override fun decodeChar(): Char { - log("decodeChar() returns Z") - return 'Z' - } + @Test + fun `decodeFromStruct() can decode a Struct with _nullable_ Boolean values`() { + @Serializable data class TestData(val isNull: Boolean?, val isNotNull: Boolean?) + val struct = encodeToStruct(TestData(isNull = null, isNotNull = true)) - override fun decodeDouble(): Double { - log("decodeDouble() returns 123.45") - return 123.45 - } + val decodedTestData = decodeFromStruct(struct) - override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { - log("decodeEnum() returns 0") - return 0 + assertThat(decodedTestData).isEqualTo(TestData(isNull = null, isNotNull = true)) } - override fun decodeFloat(): Float { - log("decodeFloat() returns 678.90") - return 678.90f - } + @Test + fun `decodeFromStruct() can decode a Struct with Int values`() { + @Serializable data class TestData(val value1: Int, val value2: Int) + val struct = encodeToStruct(TestData(value1 = 123, value2 = -456)) - override fun decodeInline(descriptor: SerialDescriptor): Decoder { - log("decodeInline() kind=${descriptor.kind} serialName=${descriptor.serialName}") - return LoggingDecoder(idBySerialDescriptor) - } + val decodedTestData = decodeFromStruct(struct) - override fun decodeInt(): Int { - log("decodeInt() returns 4242") - return 4242 + assertThat(decodedTestData).isEqualTo(TestData(value1 = 123, value2 = -456)) } - override fun decodeLong(): Long { - log("decodeLong() returns 987654") - return 987654 - } + @Test + fun `decodeFromStruct() can decode a Struct with _nullable_ Int values`() { + @Serializable data class TestData(val isNull: Int?, val isNotNull: Int?) + val struct = encodeToStruct(TestData(isNull = null, isNotNull = 42)) + + val decodedTestData = decodeFromStruct(struct) - @ExperimentalSerializationApi - override fun decodeNotNullMark(): Boolean { - log("decodeNotNullMark() returns false") - return false + assertThat(decodedTestData).isEqualTo(TestData(isNull = null, isNotNull = 42)) } - @ExperimentalSerializationApi - override fun decodeNull(): Nothing? { - log("decodeNull() returns null") - return null + @Test + fun `decodeFromStruct() can decode a Struct with extreme Int values`() { + @Serializable data class TestData(val max: Int, val min: Int) + val struct = encodeToStruct(TestData(max = Int.MAX_VALUE, min = Int.MIN_VALUE)) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(max = Int.MAX_VALUE, min = Int.MIN_VALUE)) } - override fun decodeShort(): Short { - log("decodeShort() returns 554433") - return 554433.toShort() + @Test + fun `decodeFromStruct() can decode a Struct with Double values`() { + @Serializable data class TestData(val value1: Double, val value2: Double) + val struct = encodeToStruct(TestData(value1 = 123.45, value2 = -456.78)) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(value1 = 123.45, value2 = -456.78)) } - override fun decodeString(): String { - log("decodeString() returns \"Hello World\"") - return "Hello World" + @Test + fun `decodeFromStruct() can decode a Struct with _nullable_ Double values`() { + @Serializable data class TestData(val isNull: Double?, val isNotNull: Double?) + val struct = encodeToStruct(TestData(isNull = null, isNotNull = 987.654)) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(isNull = null, isNotNull = 987.654)) } - override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int): Boolean { - log( - "decodeBooleanElement() index=$index elementName=${descriptor.getElementName(index)}" + - " returns false" + @Test + fun `decodeFromStruct() can decode a Struct with extreme Double values`() { + @Serializable + data class TestData( + val min: Double, + val max: Double, + val positiveInfinity: Double, + val negativeInfinity: Double, + val nan: Double ) - return false + val struct = + encodeToStruct( + TestData( + min = Double.MIN_VALUE, + max = Double.MAX_VALUE, + positiveInfinity = Double.POSITIVE_INFINITY, + negativeInfinity = Double.NEGATIVE_INFINITY, + nan = Double.NaN + ) + ) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo( + TestData( + min = Double.MIN_VALUE, + max = Double.MAX_VALUE, + positiveInfinity = Double.POSITIVE_INFINITY, + negativeInfinity = Double.NEGATIVE_INFINITY, + nan = Double.NaN + ) + ) } - override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte { - log( - "decodeByteElement() index=$index elementName=${descriptor.getElementName(index)}" + - " returns 66" - ) - return 66 + @Test + fun `decodeFromStruct() can decode a Struct with nested Struct values`() { + @Serializable data class TestDataA(val base: String) + @Serializable data class TestDataB(val dataA: TestDataA) + @Serializable data class TestDataC(val dataB: TestDataB) + @Serializable data class TestDataD(val dataC: TestDataC) + + val struct = encodeToStruct(TestDataD(TestDataC(TestDataB(TestDataA("hello"))))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestDataD(TestDataC(TestDataB(TestDataA("hello"))))) } - override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char { - log( - "decodeCharElement() index=$index elementName=${descriptor.getElementName(index)}" + - " returns X" - ) - return 'X' + @Test + fun `decodeFromStruct() can decode a Struct with nested _nullable_ Struct values`() { + @Serializable data class TestDataA(val base: String) + @Serializable data class TestDataB(val dataANull: TestDataA?, val dataANotNull: TestDataA?) + @Serializable data class TestDataC(val dataBNull: TestDataB?, val dataBNotNull: TestDataB?) + @Serializable data class TestDataD(val dataCNull: TestDataC?, val dataCNotNull: TestDataC?) + + val struct = + encodeToStruct(TestDataD(null, TestDataC(null, TestDataB(null, TestDataA("hello"))))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo(TestDataD(null, TestDataC(null, TestDataB(null, TestDataA("hello"))))) } - override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double { - log( - "decodeDoubleElement() index=$index elementName=${descriptor.getElementName(index)}" + - " returns 543.21" - ) - return 543.21 + @Test + fun `decodeFromStruct() can decode a Struct with nullable ListValue values`() { + @Serializable data class TestData(val nullList: List?, val nonNullList: List?) + val struct = encodeToStruct(TestData(nullList = null, nonNullList = listOf("a", "b"))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(nullList = null, nonNullList = listOf("a", "b"))) + } + + @Test + fun `decodeFromStruct() can decode a ListValue of String`() { + @Serializable data class TestData(val list: List) + val struct = encodeToStruct(TestData(listOf("elem1", "elem2"))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(listOf("elem1", "elem2"))) } - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - log("zzyzx elements=$elements") - if (elements === null || nextElementIndex >= elements.size) { - log("decodeElementIndex() returns DECODE_DONE") - return CompositeDecoder.DECODE_DONE - } - val elementIndex = nextElementIndex++ - log("decodeElementIndex() returns $elementIndex") - return elementIndex + @Test + fun `decodeFromStruct() can decode a ListValue of _nullable_ String`() { + @Serializable data class TestData(val list: List) + val struct = encodeToStruct(TestData(listOf(null, "aaa", null, "bbb"))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(listOf(null, "aaa", null, "bbb"))) } - override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float { - log( - "decodeFloatElement() index=$index elementName=${descriptor.getElementName(index)}" + - "returns 987.65" - ) - return 987.65f + @Test + fun `decodeFromStruct() can decode a ListValue of Boolean`() { + @Serializable data class TestData(val list: List) + val struct = encodeToStruct(TestData(listOf(true, false, true, false))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(listOf(true, false, true, false))) } - override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder { - log("decodeInlineElement() index=$index elementName=${descriptor.getElementName(index)}") - return LoggingDecoder(idBySerialDescriptor) + @Test + fun `decodeFromStruct() can decode a ListValue of _nullable_ Boolean`() { + @Serializable data class TestData(val list: List) + val struct = encodeToStruct(TestData(listOf(null, true, false, null, true, false))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(listOf(null, true, false, null, true, false))) } - override fun decodeIntElement(descriptor: SerialDescriptor, index: Int): Int { - log( - "decodeIntElement() index=$index elementName=${descriptor.getElementName(index)}" + - " returns 5555" - ) - return 5555 + @Test + fun `decodeFromStruct() can decode a ListValue of Int`() { + @Serializable data class TestData(val list: List) + val struct = encodeToStruct(TestData(listOf(1, 0, -1, Int.MAX_VALUE, Int.MIN_VALUE))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(listOf(1, 0, -1, Int.MAX_VALUE, Int.MIN_VALUE))) } - override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long { - log( - "decodeLongElement() index=$index elementName=${descriptor.getElementName(index)}" + - " returns 848484848" - ) - return 848484848 + @Test + fun `decodeFromStruct() can decode a ListValue of _nullable_ Int`() { + @Serializable data class TestData(val list: List) + val struct = encodeToStruct(TestData(listOf(1, 0, -1, Int.MAX_VALUE, Int.MIN_VALUE, null))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo(TestData(listOf(1, 0, -1, Int.MAX_VALUE, Int.MIN_VALUE, null))) } - override fun decodeShortElement(descriptor: SerialDescriptor, index: Int): Short { - log( - "decodeShortElement() index=$index elementName=${descriptor.getElementName(index)}" + - " returns 443344" - ) - return 443344.toShort() + @Test + fun `decodeFromStruct() can decode a ListValue of Double`() { + @Serializable data class TestData(val list: List) + val struct = + encodeToStruct( + TestData( + listOf( + 1.0, + 0.0, + -0.0, + -1.0, + Double.MAX_VALUE, + Double.MIN_VALUE, + Double.NaN, + Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY + ) + ) + ) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo( + TestData( + listOf( + 1.0, + 0.0, + -0.0, + -1.0, + Double.MAX_VALUE, + Double.MIN_VALUE, + Double.NaN, + Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY + ) + ) + ) } - override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String { - log( - "decodeStringElement() index=$index elementName=${descriptor.getElementName(index)}" + - " returns \"Goodbye Cruel World\"" - ) - return "Goodbye Cruel World" + @Test + fun `decodeFromStruct() can decode a ListValue of _nullable_ Double`() { + @Serializable data class TestData(val list: List) + val struct = + encodeToStruct( + TestData( + listOf( + 1.0, + 0.0, + -0.0, + -1.0, + Double.MAX_VALUE, + Double.MIN_VALUE, + Double.NaN, + Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, + null + ) + ) + ) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo( + TestData( + listOf( + 1.0, + 0.0, + -0.0, + -1.0, + Double.MAX_VALUE, + Double.MIN_VALUE, + Double.NaN, + Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, + null + ) + ) + ) } - @ExperimentalSerializationApi - override fun decodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy, - previousValue: T? - ): T? { - log( - "decodeNullableSerializableElement()" + - "index=$index elementName=${descriptor.getElementName(index)}" + - " previousValue=$previousValue" + - " returns null" - ) - return null + @Test + fun `decodeFromStruct() can decode a ListValue of Struct`() { + @Serializable data class TestDataA(val s1: String, val s2: String?) + @Serializable data class TestData(val list: List) + val struct = encodeToStruct(TestData(listOf(TestDataA("aa", null), TestDataA("bb", null)))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo(TestData(listOf(TestDataA("aa", null), TestDataA("bb", null)))) } - override fun decodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy, - previousValue: T? - ): T { - log( - "decodeSerializableElement()" + - "index=$index elementName=${descriptor.getElementName(index)}" + - " previousValue=$previousValue" - ) - return decodeSerializableValue(deserializer) + @Test + fun `decodeFromStruct() can decode a ListValue of _nullable_ Struct`() { + @Serializable data class TestDataA(val s1: String, val s2: String?) + @Serializable data class TestData(val list: List) + val struct = + encodeToStruct(TestData(listOf(null, TestDataA("aa", null), TestDataA("bb", null), null))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo(TestData(listOf(null, TestDataA("aa", null), TestDataA("bb", null), null))) } - companion object { + @Test + fun `decodeFromStruct() can decode a ListValue of ListValue`() { + @Serializable data class TestData(val list: List>) + val struct = encodeToStruct(TestData(listOf(listOf(1, 2, 3), listOf(4, 5, 6)))) - fun decode(serializer: DeserializationStrategy, value: Map<*, *>): T = - LoggingDecoder(value).decodeSerializableValue(serializer) + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData).isEqualTo(TestData(listOf(listOf(1, 2, 3), listOf(4, 5, 6)))) + } - inline fun decode(value: Map<*, *>): T = decode(serializer(), value) + @Test + fun `decodeFromStruct() can decode a ListValue of _nullable_ ListValue`() { + @Serializable data class TestData(val list: List?>) + val struct = encodeToStruct(TestData(listOf(listOf(1, 2, 3), listOf(4, 5, 6), null))) + + val decodedTestData = decodeFromStruct(struct) - private val nextEncoderId = AtomicLong(0) - private val nextSerialDescriptorId = AtomicLong(998800000L) + assertThat(decodedTestData).isEqualTo(TestData(listOf(listOf(1, 2, 3), listOf(4, 5, 6), null))) } } From 59cda5fe2c2a6d5f3d2f0fb078b22db3551aa127 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 17 Nov 2023 11:10:16 -0500 Subject: [PATCH 064/573] Improve error reporting when unsupported types are used --- .../dataconnect/ProtoStructDecoder.kt | 38 +++--- .../dataconnect/ProtoStructDecoderTest.kt | 122 ++++++++++++++++++ 2 files changed, 138 insertions(+), 22 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt index 4496d644c37..9537dc9e526 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt @@ -70,33 +70,27 @@ private class ProtoValueDecoder(private val valueProto: Value, private val path: override fun decodeBoolean() = valueProto.decodeBoolean(path) - override fun decodeByte(): Byte { - TODO("Not yet implemented") - } + override fun decodeByte() = notSupported() - override fun decodeChar(): Char { - TODO("Not yet implemented") - } + override fun decodeChar() = notSupported() override fun decodeDouble() = valueProto.decodeDouble(path) - override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { - TODO("Not yet implemented") - } + override fun decodeEnum(enumDescriptor: SerialDescriptor) = + notSupported("Enum (${enumDescriptor.serialName})") - override fun decodeFloat(): Float { - TODO("Not yet implemented") - } + override fun decodeFloat() = notSupported() - override fun decodeInline(descriptor: SerialDescriptor): Decoder { - TODO("Not yet implemented") - } + override fun decodeInline(descriptor: SerialDescriptor) = + notSupported("Inline (${descriptor.serialName})") override fun decodeInt(): Int = valueProto.decodeInt(path) - override fun decodeLong(): Long { - TODO("Not yet implemented") - } + override fun decodeLong() = notSupported() + + override fun decodeShort() = notSupported() + + override fun decodeString() = valueProto.decodeString(path) @ExperimentalSerializationApi override fun decodeNotNullMark(): Boolean { @@ -109,11 +103,11 @@ private class ProtoValueDecoder(private val valueProto: Value, private val path: return null } - override fun decodeShort(): Short { - TODO("Not yet implemented") + private companion object { + inline fun notSupported(): Nothing = notSupported(T::class.qualifiedName!!) + fun notSupported(unsupportedTypeName: String): Nothing = + throw SerializationException("decoding $unsupportedTypeName is not supported") } - - override fun decodeString() = valueProto.decodeString(path) } private class ProtoStructValueDecoder(private val struct: Struct, private val path: String?) : diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt index ab41057119a..f5a469638ca 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt @@ -17,12 +17,23 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat +import com.google.protobuf.Struct +import com.google.protobuf.Value.KindCase +import java.util.regex.Pattern import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import org.junit.Assert.assertThrows import org.junit.Test class ProtoStructDecoderTest { + @Test + fun `decodeFromStruct() can decode a Struct to Unit`() { + val decodedTestData = decodeFromStruct(Struct.getDefaultInstance()) + assertThat(decodedTestData).isSameInstanceAs(Unit) + } + @Test fun `decodeFromStruct() can decode a Struct with String values`() { @Serializable data class TestData(val value1: String, val value2: String) @@ -375,4 +386,115 @@ class ProtoStructDecoderTest { assertThat(decodedTestData).isEqualTo(TestData(listOf(listOf(1, 2, 3), listOf(4, 5, 6), null))) } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode an Int`() { + assertThrowsExpectedDifferentKindCase( + expectedKind = KindCase.NUMBER_VALUE, + actualKind = KindCase.STRUCT_VALUE + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a Double`() { + assertThrowsExpectedDifferentKindCase( + expectedKind = KindCase.NUMBER_VALUE, + actualKind = KindCase.STRUCT_VALUE + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a Boolean`() { + assertThrowsExpectedDifferentKindCase( + expectedKind = KindCase.BOOL_VALUE, + actualKind = KindCase.STRUCT_VALUE + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a String`() { + assertThrowsExpectedDifferentKindCase( + expectedKind = KindCase.STRING_VALUE, + actualKind = KindCase.STRUCT_VALUE + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a List`() { + assertThrowsExpectedDifferentKindCase>( + expectedKind = KindCase.LIST_VALUE, + actualKind = KindCase.STRUCT_VALUE + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a Byte`() { + assertThrowsNotSupported() + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a Char`() { + assertThrowsNotSupported() + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a Enum`() { + assertThrowsNotSupported() + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a Float`() { + assertThrowsNotSupported() + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a Inline`() { + assertThrowsNotSupported() + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a Long`() { + assertThrowsNotSupported() + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode a Short`() { + assertThrowsNotSupported() + } } + +private inline fun assertThrowsExpectedDifferentKindCase( + expectedKind: KindCase, + actualKind: KindCase +) { + val exception = + assertThrows(SerializationException::class.java) { + decodeFromStruct(Struct.getDefaultInstance()) + } + assertThat(exception).hasMessageThat().ignoringCase().contains("expected $expectedKind") + assertThat(exception).hasMessageThat().ignoringCase().contains("got $actualKind") +} + +private inline fun assertThrowsNotSupported() { + val exception = + assertThrows(SerializationException::class.java) { + decodeFromStruct(Struct.getDefaultInstance()) + } + assertThat(exception) + .hasMessageThat() + .containsMatch( + Pattern.compile( + "decoding.*${Pattern.quote(T::class.qualifiedName!!)}.*not supported", + Pattern.CASE_INSENSITIVE + ) + ) +} + +private enum class TestEnum { + A, + B, + C, + D +} + +@Serializable @JvmInline value class TestValueClass(val a: Int) From 017a83db42c7cc5d80edea404c82aeb7a0be8959 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 17 Nov 2023 11:41:51 -0500 Subject: [PATCH 065/573] add inline class support --- .../dataconnect/ProtoStructDecoder.kt | 22 ++- .../dataconnect/ProtoStructEncoder.kt | 4 +- .../dataconnect/ProtoStructDecoderTest.kt | 140 ++++++++++++++++-- 3 files changed, 139 insertions(+), 27 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt index 9537dc9e526..f2f5a8fa02e 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt @@ -81,8 +81,7 @@ private class ProtoValueDecoder(private val valueProto: Value, private val path: override fun decodeFloat() = notSupported() - override fun decodeInline(descriptor: SerialDescriptor) = - notSupported("Inline (${descriptor.serialName})") + override fun decodeInline(descriptor: SerialDescriptor) = ProtoValueDecoder(valueProto, path) override fun decodeInt(): Int = valueProto.decodeInt(path) @@ -150,9 +149,8 @@ private class ProtoStructValueDecoder(private val struct: Struct, private val pa TODO("Not yet implemented") } - override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder { - TODO("Not yet implemented") - } + override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(descriptor, index, ::ProtoValueDecoder) override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(descriptor, index, Value::decodeInt) @@ -174,12 +172,13 @@ private class ProtoStructValueDecoder(private val struct: Struct, private val pa block: Value.(String?) -> T ): T { val elementName = descriptor.getElementName(index) + val elementPath = elementPathForName(elementName) val value = struct.fieldsMap[elementName] ?: throw SerializationException( - "element \"$elementName\" missing (expected ${descriptor.getElementDescriptor(index).kind})" + "element \"$elementPath\" missing (expected ${descriptor.getElementDescriptor(index).kind})" ) - return block(value, elementPathForName(elementName)) + return block(value, elementPath) } @ExperimentalSerializationApi @@ -250,16 +249,15 @@ private class ProtoListValueDecoder(private val list: ListValue, private val pat TODO("Not yet implemented") } - override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double = + override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(index, Value::decodeDouble) override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float { TODO("Not yet implemented") } - override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder { - TODO("Not yet implemented") - } + override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, ::ProtoValueDecoder) override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(index, Value::decodeInt) @@ -272,7 +270,7 @@ private class ProtoListValueDecoder(private val list: ListValue, private val pat TODO("Not yet implemented") } - override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String = + override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(index, Value::decodeString) private inline fun decodeValueElement(index: Int, block: Value.(String?) -> T): T = diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt index e020ab81d1a..407bb77b0db 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt @@ -73,9 +73,7 @@ private class ProtoValueEncoder : Encoder { TODO("Not yet implemented") } - override fun encodeInline(descriptor: SerialDescriptor): Encoder { - TODO("Not yet implemented") - } + override fun encodeInline(descriptor: SerialDescriptor) = this override fun encodeInt(value: Int) { encodeDouble(value.toDouble()) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt index f5a469638ca..1cd5a6939ed 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt @@ -18,8 +18,10 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat import com.google.protobuf.Struct +import com.google.protobuf.Value import com.google.protobuf.Value.KindCase import java.util.regex.Pattern +import kotlin.reflect.KClass import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException @@ -387,9 +389,86 @@ class ProtoStructDecoderTest { assertThat(decodedTestData).isEqualTo(TestData(listOf(listOf(1, 2, 3), listOf(4, 5, 6), null))) } + @Test + fun `decodeFromStruct() can decode a Struct with Inline values`() { + @Serializable data class TestData(val s: TestStringValueClass, val i: TestIntValueClass) + val struct = encodeToStruct(TestData(TestStringValueClass("TestString"), TestIntValueClass(42))) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo(TestData(TestStringValueClass("TestString"), TestIntValueClass(42))) + } + + @Test + fun `decodeFromStruct() can decode a Struct with _nullable_ Inline values`() { + @Serializable + data class TestData( + val s: TestStringValueClass?, + val snull: TestStringValueClass?, + val i: TestIntValueClass?, + val inull: TestIntValueClass? + ) + val struct = + encodeToStruct( + TestData(TestStringValueClass("TestString"), null, TestIntValueClass(42), null) + ) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo(TestData(TestStringValueClass("TestString"), null, TestIntValueClass(42), null)) + } + + @Test + fun `decodeFromStruct() can decode a ListValue with Inline values`() { + @Serializable + data class TestData(val s: List, val i: List) + val struct = + encodeToStruct( + TestData( + listOf(TestStringValueClass("TestString1"), TestStringValueClass("TestString2")), + listOf(TestIntValueClass(42), TestIntValueClass(43)) + ) + ) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo( + TestData( + listOf(TestStringValueClass("TestString1"), TestStringValueClass("TestString2")), + listOf(TestIntValueClass(42), TestIntValueClass(43)) + ) + ) + } + + @Test + fun `decodeFromStruct() can decode a ListValue with _nullable_ Inline values`() { + @Serializable + data class TestData(val s: List, val i: List) + val struct = + encodeToStruct( + TestData( + listOf(TestStringValueClass("TestString1"), null, TestStringValueClass("TestString2")), + listOf(TestIntValueClass(42), null, TestIntValueClass(43)) + ) + ) + + val decodedTestData = decodeFromStruct(struct) + + assertThat(decodedTestData) + .isEqualTo( + TestData( + listOf(TestStringValueClass("TestString1"), null, TestStringValueClass("TestString2")), + listOf(TestIntValueClass(42), null, TestIntValueClass(43)) + ) + ) + } + @Test fun `decodeFromStruct() should throw SerializationException if attempting to decode an Int`() { - assertThrowsExpectedDifferentKindCase( + assertDecodeFromStructThrowsIncorrectKindCase( expectedKind = KindCase.NUMBER_VALUE, actualKind = KindCase.STRUCT_VALUE ) @@ -397,7 +476,7 @@ class ProtoStructDecoderTest { @Test fun `decodeFromStruct() should throw SerializationException if attempting to decode a Double`() { - assertThrowsExpectedDifferentKindCase( + assertDecodeFromStructThrowsIncorrectKindCase( expectedKind = KindCase.NUMBER_VALUE, actualKind = KindCase.STRUCT_VALUE ) @@ -405,7 +484,7 @@ class ProtoStructDecoderTest { @Test fun `decodeFromStruct() should throw SerializationException if attempting to decode a Boolean`() { - assertThrowsExpectedDifferentKindCase( + assertDecodeFromStructThrowsIncorrectKindCase( expectedKind = KindCase.BOOL_VALUE, actualKind = KindCase.STRUCT_VALUE ) @@ -413,7 +492,7 @@ class ProtoStructDecoderTest { @Test fun `decodeFromStruct() should throw SerializationException if attempting to decode a String`() { - assertThrowsExpectedDifferentKindCase( + assertDecodeFromStructThrowsIncorrectKindCase( expectedKind = KindCase.STRING_VALUE, actualKind = KindCase.STRUCT_VALUE ) @@ -421,7 +500,7 @@ class ProtoStructDecoderTest { @Test fun `decodeFromStruct() should throw SerializationException if attempting to decode a List`() { - assertThrowsExpectedDifferentKindCase>( + assertDecodeFromStructThrowsIncorrectKindCase>( expectedKind = KindCase.LIST_VALUE, actualKind = KindCase.STRUCT_VALUE ) @@ -448,8 +527,20 @@ class ProtoStructDecoderTest { } @Test - fun `decodeFromStruct() should throw SerializationException if attempting to decode a Inline`() { - assertThrowsNotSupported() + fun `decodeFromStruct() should throw SerializationException if attempting to decode an Inline of supported type`() { + assertDecodeFromStructThrowsIncorrectKindCase( + expectedKind = KindCase.STRING_VALUE, + actualKind = KindCase.STRUCT_VALUE + ) + assertDecodeFromStructThrowsIncorrectKindCase( + expectedKind = KindCase.NUMBER_VALUE, + actualKind = KindCase.STRUCT_VALUE + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if attempting to decode an Inline of _unsupported_ type`() { + assertThrowsNotSupported(expectedTypeInMessage = Byte::class) } @Test @@ -463,19 +554,40 @@ class ProtoStructDecoderTest { } } -private inline fun assertThrowsExpectedDifferentKindCase( +/** + * Asserts that `decodeFromStruct` throws [SerializationException], with a message that indicates + * that the "kind" of the [Value] being decoded differed from what was expected. + * + * @param expectedKind The expected "kind" of the [Value] being decoded that should be incorporated + * into the exception's message. + * @param actualKind The actual "kind" of the [Value] being decoded that should be incorporated into + * the exception's message. + */ +private inline fun assertDecodeFromStructThrowsIncorrectKindCase( expectedKind: KindCase, - actualKind: KindCase + actualKind: KindCase, ) { val exception = assertThrows(SerializationException::class.java) { decodeFromStruct(Struct.getDefaultInstance()) } + // The error message is expected to look something like this: + // "expected NUMBER_VALUE, but got STRUCT_VALUE" assertThat(exception).hasMessageThat().ignoringCase().contains("expected $expectedKind") assertThat(exception).hasMessageThat().ignoringCase().contains("got $actualKind") } -private inline fun assertThrowsNotSupported() { +/** + * Asserts that `decodeFromStruct` throws [SerializationException], with a message that indicates + * that the type `T` being decoded is not supported. + * + * @param expectedTypeInMessage The type that the exception's message should indicate is not + * supported; if not specified, use `T`. Note that the only case where this argument's value should + * be anything _other_ than `T` is for _value classes_ that are mapped to a primitive type. + */ +private inline fun assertThrowsNotSupported( + expectedTypeInMessage: KClass<*> = T::class +) { val exception = assertThrows(SerializationException::class.java) { decodeFromStruct(Struct.getDefaultInstance()) @@ -484,7 +596,7 @@ private inline fun assertThrowsNotSupported() { .hasMessageThat() .containsMatch( Pattern.compile( - "decoding.*${Pattern.quote(T::class.qualifiedName!!)}.*not supported", + "decoding.*${Pattern.quote(expectedTypeInMessage.qualifiedName!!)}.*not supported", Pattern.CASE_INSENSITIVE ) ) @@ -497,4 +609,8 @@ private enum class TestEnum { D } -@Serializable @JvmInline value class TestValueClass(val a: Int) +@Serializable @JvmInline value class TestStringValueClass(val a: String) + +@Serializable @JvmInline value class TestIntValueClass(val a: Int) + +@Serializable @JvmInline value class TestByteValueClass(val a: Byte) From 5510a1f51ba8cc70acd3e860c81373fbaeb7dd11 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 17 Nov 2023 15:40:04 -0500 Subject: [PATCH 066/573] ProtoStructDecoder.kt minor improvements --- .../dataconnect/ProtoStructDecoder.kt | 18 ++- .../dataconnect/ProtoStructDecoderTest.kt | 140 +++++++++++++++++- 2 files changed, 149 insertions(+), 9 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt index f2f5a8fa02e..95ff6512289 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt @@ -25,11 +25,22 @@ fun decodeFromStruct(deserializer: DeserializationStrategy, struct: Struc return ProtoValueDecoder(protoValue, path = null).decodeSerializableValue(deserializer) } +private fun Value.toAny(): Any? = + when (kindCase) { + KindCase.BOOL_VALUE -> boolValue + KindCase.NUMBER_VALUE -> numberValue + KindCase.STRING_VALUE -> stringValue + KindCase.LIST_VALUE -> listValue.valuesList + KindCase.STRUCT_VALUE -> structValue.fieldsMap + KindCase.NULL_VALUE -> null + else -> "ERROR: unsupported kindCase: $kindCase" + } + private fun Value.decode(path: String?, expectedKindCase: KindCase, block: (Value) -> T): T = if (kindCase != expectedKindCase) { throw SerializationException( (if (path === null) "" else "decoding \"$path\" failed: ") + - "expected $expectedKindCase, but got $kindCase" + "expected $expectedKindCase, but got $kindCase (${toAny()})" ) } else { block(this) @@ -211,14 +222,15 @@ private class ProtoStructValueDecoder(private val struct: Struct, private val pa } val elementName = descriptor.getElementName(index) + val elementPath = elementPathForName(elementName) val valueProto = struct.fieldsMap[elementName] if (valueProto === null) { throw SerializationException( - "element \"$elementName\" missing; expected ${descriptor.getElementDescriptor(index).kind}" + "element \"$elementPath\" missing; expected ${descriptor.getElementDescriptor(index).kind}" ) } - return deserializer.deserialize(ProtoValueDecoder(valueProto, elementPathForName(elementName))) + return deserializer.deserialize(ProtoValueDecoder(valueProto, elementPath)) } private fun elementPathForName(elementName: String) = diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt index 1cd5a6939ed..2c29350b749 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt @@ -17,9 +17,12 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat +import com.google.protobuf.NullValue import com.google.protobuf.Struct import com.google.protobuf.Value import com.google.protobuf.Value.KindCase +import com.google.protobuf.struct +import com.google.protobuf.value import java.util.regex.Pattern import kotlin.reflect.KClass import kotlinx.serialization.ExperimentalSerializationApi @@ -470,7 +473,7 @@ class ProtoStructDecoderTest { fun `decodeFromStruct() should throw SerializationException if attempting to decode an Int`() { assertDecodeFromStructThrowsIncorrectKindCase( expectedKind = KindCase.NUMBER_VALUE, - actualKind = KindCase.STRUCT_VALUE + actualKind = KindCase.STRUCT_VALUE, ) } @@ -478,7 +481,7 @@ class ProtoStructDecoderTest { fun `decodeFromStruct() should throw SerializationException if attempting to decode a Double`() { assertDecodeFromStructThrowsIncorrectKindCase( expectedKind = KindCase.NUMBER_VALUE, - actualKind = KindCase.STRUCT_VALUE + actualKind = KindCase.STRUCT_VALUE, ) } @@ -552,6 +555,127 @@ class ProtoStructDecoderTest { fun `decodeFromStruct() should throw SerializationException if attempting to decode a Short`() { assertThrowsNotSupported() } + + @Test + fun `decodeFromStruct() should throw SerializationException if decoding a Boolean value found a different type`() { + @Serializable data class TestEncodeSubData(val someValue: String) + @Serializable data class TestEncodeData(val aaa: TestEncodeSubData) + @Serializable data class TestDecodeSubData(val someValue: Boolean) + @Serializable data class TestDecodeData(val aaa: TestDecodeSubData) + + assertDecodeFromStructThrowsIncorrectKindCase( + expectedKind = KindCase.BOOL_VALUE, + actualKind = KindCase.STRING_VALUE, + struct = encodeToStruct(TestEncodeData(TestEncodeSubData("foo"))), + actualValue = "foo", + path = "aaa.someValue" + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if decoding an Int value found a different type`() { + @Serializable data class TestEncodeSubData(val someValue: String) + @Serializable data class TestEncodeData(val aaa: TestEncodeSubData) + @Serializable data class TestDecodeSubData(val someValue: Int) + @Serializable data class TestDecodeData(val aaa: TestDecodeSubData) + + assertDecodeFromStructThrowsIncorrectKindCase( + expectedKind = KindCase.NUMBER_VALUE, + actualKind = KindCase.STRING_VALUE, + struct = encodeToStruct(TestEncodeData(TestEncodeSubData("foo"))), + actualValue = "foo", + path = "aaa.someValue" + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if decoding a Double value found a different type`() { + @Serializable data class TestEncodeSubData(val someValue: String) + @Serializable data class TestEncodeData(val aaa: TestEncodeSubData) + @Serializable data class TestDecodeSubData(val someValue: Double) + @Serializable data class TestDecodeData(val aaa: TestDecodeSubData) + + assertDecodeFromStructThrowsIncorrectKindCase( + expectedKind = KindCase.NUMBER_VALUE, + actualKind = KindCase.STRING_VALUE, + struct = encodeToStruct(TestEncodeData(TestEncodeSubData("foo"))), + actualValue = "foo", + path = "aaa.someValue" + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if decoding a String value found a different type`() { + @Serializable data class TestEncodeSubData(val someValue: Int) + @Serializable data class TestEncodeData(val aaa: TestEncodeSubData) + @Serializable data class TestDecodeSubData(val someValue: String) + @Serializable data class TestDecodeData(val aaa: TestDecodeSubData) + + assertDecodeFromStructThrowsIncorrectKindCase( + expectedKind = KindCase.STRING_VALUE, + actualKind = KindCase.NUMBER_VALUE, + struct = encodeToStruct(TestEncodeData(TestEncodeSubData(42))), + actualValue = 42.0, + path = "aaa.someValue" + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if decoding a List value found a different type`() { + @Serializable data class TestEncodeSubData(val someValue: Boolean) + @Serializable data class TestEncodeData(val aaa: TestEncodeSubData) + @Serializable data class TestDecodeSubData(val someValue: List) + @Serializable data class TestDecodeData(val aaa: TestDecodeSubData) + + assertDecodeFromStructThrowsIncorrectKindCase( + expectedKind = KindCase.LIST_VALUE, + actualKind = KindCase.BOOL_VALUE, + struct = encodeToStruct(TestEncodeData(TestEncodeSubData(true))), + actualValue = true, + path = "aaa.someValue" + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if decoding a Struct value found a different type`() { + @Serializable data class TestEncodeSubData(val someValue: Int) + @Serializable data class TestEncodeData(val aaa: TestEncodeSubData) + @Serializable data class TestDecodeSubData2(val someValue: Int) + @Serializable data class TestDecodeSubData(val someValue: TestDecodeSubData2) + @Serializable data class TestDecodeData(val aaa: TestDecodeSubData) + + assertDecodeFromStructThrowsIncorrectKindCase( + expectedKind = KindCase.STRUCT_VALUE, + actualKind = KindCase.NUMBER_VALUE, + struct = encodeToStruct(TestEncodeData(TestEncodeSubData(42))), + actualValue = 42.0, + path = "aaa.someValue" + ) + } + + @Test + fun `decodeFromStruct() should throw SerializationException if decoding a Struct value found null`() { + @Serializable data class TestDecodeSubData2(val someValue: String) + @Serializable data class TestDecodeSubData(val bbb: TestDecodeSubData2) + @Serializable data class TestDecodeData(val aaa: TestDecodeSubData) + val struct = struct { + fields["aaa"] = value { + structValue = struct { fields["bbb"] = value { nullValue = NullValue.NULL_VALUE } } + } + } + + assertDecodeFromStructThrowsIncorrectKindCase( + expectedKind = KindCase.STRUCT_VALUE, + actualKind = KindCase.NULL_VALUE, + struct = struct, + actualValue = null, + path = "aaa.bbb" + ) + } + + // TODO: Add tests for decoding to objects with unsupported field types (e.g. Byte, Char) and + // list elements of unsupported field types (e.g. Byte, Char). + } /** @@ -566,15 +690,19 @@ class ProtoStructDecoderTest { private inline fun assertDecodeFromStructThrowsIncorrectKindCase( expectedKind: KindCase, actualKind: KindCase, + actualValue: Any? = Struct.getDefaultInstance().fieldsMap, + struct: Struct = Struct.getDefaultInstance(), + path: String? = null ) { - val exception = - assertThrows(SerializationException::class.java) { - decodeFromStruct(Struct.getDefaultInstance()) - } + val exception = assertThrows(SerializationException::class.java) { decodeFromStruct(struct) } // The error message is expected to look something like this: // "expected NUMBER_VALUE, but got STRUCT_VALUE" assertThat(exception).hasMessageThat().ignoringCase().contains("expected $expectedKind") assertThat(exception).hasMessageThat().ignoringCase().contains("got $actualKind") + assertThat(exception).hasMessageThat().ignoringCase().contains("($actualValue)") + if (path !== null) { + assertThat(exception).hasMessageThat().ignoringCase().contains("decoding \"$path\"") + } } /** From da4316f6e1c38d525459bc916ea49815aa6c5842 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 20 Nov 2023 11:01:44 -0500 Subject: [PATCH 067/573] Rename "Result" to "Data" --- .../dataconnect/FirebaseDataConnectTest.kt | 101 ------------------ .../dataconnect/QuerySubscriptionTest.kt | 29 +++-- .../dataconnect/generated/PostsTest.kt | 16 +-- .../testutil/schemas/PersonSchema.kt | 16 +-- .../testutil/schemas/PersonSchemaTest.kt | 6 +- .../google/firebase/dataconnect/BaseRef.kt | 6 +- .../dataconnect/DataConnectGrpcClient.kt | 16 +-- .../dataconnect/FirebaseDataConnect.kt | 35 +++--- .../firebase/dataconnect/MutationRef.kt | 10 +- .../google/firebase/dataconnect/QueryRef.kt | 12 +-- .../firebase/dataconnect/QuerySubscription.kt | 29 +++-- .../apiproposal/QueryApiProposal.kt | 67 +++++------- .../dataconnect/generated/GetPostQuery.kt | 12 +-- 13 files changed, 118 insertions(+), 237 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index a96a18210b6..2363f4d542b 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -23,15 +23,10 @@ import com.google.firebase.app import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory import com.google.firebase.dataconnect.testutil.TestFirebaseAppFactory -import com.google.firebase.dataconnect.testutil.installEmulatorSchema -import java.util.UUID import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.thread import kotlin.concurrent.withLock -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.Serializable import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test @@ -299,100 +294,4 @@ class FirebaseDataConnectTest { assertThat(toStringResult).containsMatch("location=TestLocation\\W") assertThat(toStringResult).containsMatch("service=TestService\\W") } - - @Test - fun helloWorld() = runTest { - val dc = dataConnectFactory.newInstance(service = "local") - - val postId = "${UUID.randomUUID()}" - val postContent = "${System.currentTimeMillis()}" - - @Serializable data class PostData(val id: String, val content: String) - @Serializable data class CreatePostVariables(val data: PostData) - @Serializable data class GetPostVariables(val id: String) - - @Serializable data class GetPostResultPost(val content: String, val comments: List) - @Serializable data class GetPostResult(val post: GetPostResultPost) - - run { - val mutation = - dc.mutation( - operationName = "createPost", - operationSet = "crud", - revision = "TestRevision", - ) - val mutationResponse = - mutation.execute(CreatePostVariables(PostData(id = postId, content = postContent))) - assertWithMessage("mutationResponse").that(mutationResponse).isSameInstanceAs(Unit) - } - - run { - val query = - dc.query( - operationName = "getPost", - operationSet = "crud", - revision = "TestRevision", - ) - val queryResult = query.execute(GetPostVariables(id = postId)) - assertWithMessage("queryResponse") - .that(queryResult) - .isEqualTo(GetPostResult(GetPostResultPost(content = postContent, comments = emptyList()))) - } - } - - @Test - fun testInstallEmulatorSchema() { - @Serializable data class PersonData(val id: String, val name: String, val age: Int? = null) - @Serializable data class CreatePersonVariables(val data: PersonData) - @Serializable data class GetPersonVariables(val id: String) - @Serializable data class GetPersonResultPerson(val name: String, val age: Int?) - @Serializable data class GetPersonResult(val person: GetPersonResultPerson) - @Serializable - data class GetAllPeopleResultPerson(val id: String, val name: String, val age: Int?) - @Serializable data class GetAllPeopleResult(val people: List) - - suspend fun FirebaseDataConnect.createPerson(id: String, name: String, age: Int? = null) = - mutation( - operationName = "createPerson", - operationSet = "ops", - revision = "42", - ) - .execute(CreatePersonVariables(PersonData(id = id, name = name, age = age))) - - suspend fun FirebaseDataConnect.getPerson(id: String) = - query( - operationName = "getPerson", - operationSet = "ops", - revision = "42", - ) - .execute(GetPersonVariables(id = id)) - - suspend fun FirebaseDataConnect.getAllPeople() = - query( - operationName = "getAllPeople", - operationSet = "ops", - revision = "42", - ) - .execute(Unit) - - val dataConnect = dataConnectFactory.newInstance() - - runBlocking { - dataConnect.installEmulatorSchema("testing_graphql_schemas/person") - - dataConnect.createPerson(id = "TestId1", name = "TestName1") - dataConnect.createPerson(id = "TestId2", name = "TestName2", age = 999) - - assertThat(dataConnect.getPerson(id = "TestId1")) - .isEqualTo(GetPersonResult(GetPersonResultPerson(name = "TestName1", age = null))) - assertThat(dataConnect.getPerson(id = "TestId2")) - .isEqualTo(GetPersonResult(GetPersonResultPerson(name = "TestName2", age = 999))) - - assertThat(dataConnect.getAllPeople().people) - .containsExactly( - GetAllPeopleResultPerson(id = "TestId1", name = "TestName1", age = null), - GetAllPeopleResultPerson(id = "TestId2", name = "TestName2", age = 999), - ) - } - } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt index 4bb78c104b9..16cd2373276 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt @@ -71,10 +71,9 @@ class QuerySubscriptionTest { val querySubscription = schema.getPerson.subscribe(id = "TestId12345") withTimeout(2.seconds) { - val resultsChannel = - Channel(capacity = Channel.UNLIMITED) + val resultsChannel = Channel(capacity = Channel.UNLIMITED) val collectJob = launch { - querySubscription.flow.collect { resultsChannel.send(it.result.getOrThrow()) } + querySubscription.flow.collect { resultsChannel.send(it.data.getOrThrow()) } } val result1 = resultsChannel.receive() @@ -96,12 +95,12 @@ class QuerySubscriptionTest { val querySubscription = schema.getPerson.subscribe(id = "TestId12345") withTimeout(2.seconds) { - val result1 = querySubscription.flow.first().result.getOrThrow() + val result1 = querySubscription.flow.first().data.getOrThrow() assertThat(result1).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) schema.updatePerson.execute(id = "TestId12345", name = "TestName2", age = 10002) - val result2 = querySubscription.flow.first().result.getOrThrow() + val result2 = querySubscription.flow.first().data.getOrThrow() assertThat(result2).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) } } @@ -118,7 +117,7 @@ class QuerySubscriptionTest { repeat(5) { assertWithMessage("fast flow retrieval iteration $it") - .that(querySubscription.flow.first().result.getOrThrow()) + .that(querySubscription.flow.first().data.getOrThrow()) .isEqualToGetPersonQueryResult(name = "TestName", age = 10000) } @@ -132,15 +131,13 @@ class QuerySubscriptionTest { val querySubscription = schema.getPerson.subscribe(id = "TestId12345") withTimeout(2.seconds) { - val resultsChannel1 = - Channel(capacity = Channel.UNLIMITED) + val resultsChannel1 = Channel(capacity = Channel.UNLIMITED) val flowJob1 = launch { - querySubscription.flow.collect { resultsChannel1.send(it.result.getOrThrow()) } + querySubscription.flow.collect { resultsChannel1.send(it.data.getOrThrow()) } } - val resultsChannel2 = - Channel(capacity = Channel.UNLIMITED) + val resultsChannel2 = Channel(capacity = Channel.UNLIMITED) val flowJob2 = launch { - querySubscription.flow.collect { resultsChannel2.send(it.result.getOrThrow()) } + querySubscription.flow.collect { resultsChannel2.send(it.data.getOrThrow()) } } resultsChannel1.purge(0.25.seconds).forEach { assertThat(it).isEqualToGetPersonQueryResult(name = "TestName0", age = 10000) @@ -169,9 +166,9 @@ class QuerySubscriptionTest { withTimeout(5.seconds) { val resultsChannel = - Channel>(capacity = Channel.UNLIMITED) + Channel>(capacity = Channel.UNLIMITED) val collectJob = launch { - querySubscription.flow.map { it.result }.collect(resultsChannel::send) + querySubscription.flow.map { it.data }.collect(resultsChannel::send) } val maxHardwareConcurrency = Math.max(2, Runtime.getRuntime().availableProcessors()) @@ -197,8 +194,8 @@ class QuerySubscriptionTest { private fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = isEqualTo( - PersonSchema.GetPersonQuery.Result( - PersonSchema.GetPersonQuery.Result.Person(name = name, age = age) + PersonSchema.GetPersonQuery.Data( + PersonSchema.GetPersonQuery.Data.Person(name = name, age = age) ) ) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt index fbcf1dac96c..5f3e4b8ba02 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt @@ -61,8 +61,8 @@ class PostsTest { val queryResponse = dc.queries.getPost.execute(id = postId) assertWithMessage("queryResponse") - .that(queryResponse?.post) - .isEqualTo(GetPostQuery.Result.Post(content = postContent, comments = emptyList())) + .that(queryResponse.post) + .isEqualTo(GetPostQuery.Data.Post(content = postContent, comments = emptyList())) } } @@ -83,9 +83,9 @@ class PostsTest { assertWithMessage("lastResult 0").that(querySubscription.lastResult).isNull() val result1 = querySubscription.flow.timeout(5.seconds).first() - assertWithMessage("result1.isSuccess").that(result1.result.isSuccess).isTrue() + assertWithMessage("result1.isSuccess").that(result1.data.isSuccess).isTrue() assertWithMessage("result1.post.content") - .that(result1.result.getOrThrow()?.post?.content) + .that(result1.data.getOrThrow().post?.content) .isEqualTo(postContent1) assertWithMessage("lastResult 1").that(querySubscription.lastResult).isEqualTo(result1) @@ -96,13 +96,13 @@ class PostsTest { val results2 = flow2Job.await() assertWithMessage("results2.size").that(results2.size).isEqualTo(2) - assertWithMessage("results2[0].isSuccess").that(results2[0].result.isSuccess).isTrue() - assertWithMessage("results2[1].isSuccess").that(results2[1].result.isSuccess).isTrue() + assertWithMessage("results2[0].isSuccess").that(results2[0].data.isSuccess).isTrue() + assertWithMessage("results2[1].isSuccess").that(results2[1].data.isSuccess).isTrue() assertWithMessage("results2[0].post.content") - .that(results2[0].result.getOrThrow()?.post?.content) + .that(results2[0].data.getOrThrow().post?.content) .isEqualTo(postContent1) assertWithMessage("results2[1].post.content") - .that(results2[1].result.getOrThrow()?.post?.content) + .that(results2[1].data.getOrThrow().post?.content) .isEqualTo(postContent2) assertWithMessage("lastResult 2").that(querySubscription.lastResult).isEqualTo(results2[1]) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index fc8be52cadb..33558004a43 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -64,7 +64,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { } val getPerson = - dataConnect.query( + dataConnect.query( operationName = "getPerson", operationSet = "ops", revision = "42", @@ -75,13 +75,13 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { data class Variables(val id: String, val name: String? = null, val age: Int? = null) @Serializable - data class Result(val person: Person?) { + data class Data(val person: Person?) { @Serializable data class Person(val name: String, val age: Int? = null) } } val getAllPeople = - dataConnect.query( + dataConnect.query( operationName = "getAllPeople", operationSet = "ops", revision = "42", @@ -89,7 +89,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { class GetAllPeopleQuery private constructor() { @Serializable - data class Result(val people: List) { + data class Data(val people: List) { @Serializable data class Person(val id: String, val name: String, val age: Int?) } } @@ -128,15 +128,15 @@ object DeletePersonMutationExt { } object GetPersonQueryExt { - suspend fun QueryRef + suspend fun QueryRef .execute(id: String) = execute(PersonSchema.GetPersonQuery.Variables(id = id)) - fun QueryRef.subscribe( + fun QueryRef.subscribe( id: String ) = subscribe(PersonSchema.GetPersonQuery.Variables(id = id)) } object GetAllPeoplePersonQueryExt { - suspend fun QueryRef.execute() = execute(Unit) - fun QueryRef.subscribe() = subscribe(Unit) + suspend fun QueryRef.execute() = execute(Unit) + fun QueryRef.subscribe() = subscribe(Unit) } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt index 41fcab8e17c..f94c9f64f0b 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt @@ -102,9 +102,9 @@ class PersonSchemaTest { assertThat(result.people) .containsExactly( - PersonSchema.GetAllPeopleQuery.Result.Person(id = "111", name = "Name111", age = 111), - PersonSchema.GetAllPeopleQuery.Result.Person(id = "222", name = "Name222", age = 222), - PersonSchema.GetAllPeopleQuery.Result.Person(id = "333", name = "Name333", age = null), + PersonSchema.GetAllPeopleQuery.Data.Person(id = "111", name = "Name111", age = 111), + PersonSchema.GetAllPeopleQuery.Data.Person(id = "222", name = "Name222", age = 222), + PersonSchema.GetAllPeopleQuery.Data.Person(id = "333", name = "Name333", age = null), ) } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt index eebd4c5a04b..783455eb560 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt @@ -16,14 +16,14 @@ package com.google.firebase.dataconnect import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy -abstract class BaseRef +abstract class BaseRef internal constructor( val dataConnect: FirebaseDataConnect, internal val operationName: String, internal val operationSet: String, internal val revision: String, internal val variablesSerializer: SerializationStrategy, - internal val resultDeserializer: DeserializationStrategy, + internal val dataDeserializer: DeserializationStrategy, ) { - abstract suspend fun execute(variables: VariablesType): ResultType + abstract suspend fun execute(variables: VariablesType): DataType } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index de38b213b83..7b3517dbe2d 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -78,14 +78,14 @@ internal class DataConnectGrpcClient( private val grpcStub: DataServiceCoroutineStub by lazy { DataServiceCoroutineStub(grpcChannel) } - suspend fun executeQuery( + suspend fun executeQuery( operationSet: String, operationName: String, revision: String, variables: VariablesType, variablesSerializer: SerializationStrategy, - resultDeserializer: DeserializationStrategy - ): ResultType { + dataDeserializer: DeserializationStrategy + ): DataType { val request = executeQueryRequest { this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName @@ -108,17 +108,17 @@ internal class DataConnectGrpcClient( ) } - return decodeFromStruct(resultDeserializer, response.data) + return decodeFromStruct(dataDeserializer, response.data) } - suspend fun executeMutation( + suspend fun executeMutation( operationSet: String, operationName: String, revision: String, variables: VariablesType, variablesSerializer: SerializationStrategy, - resultDeserializer: DeserializationStrategy - ): ResultType { + dataDeserializer: DeserializationStrategy + ): DataType { val request = executeMutationRequest { this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName @@ -141,7 +141,7 @@ internal class DataConnectGrpcClient( ) } - return decodeFromStruct(resultDeserializer, response.data) + return decodeFromStruct(dataDeserializer, response.data) } override fun toString(): String { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 192b0feb666..c6ef0854cd5 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -101,7 +101,7 @@ internal constructor( revision = ref.revision, variables = variables, variablesSerializer = ref.variablesSerializer, - resultDeserializer = ref.resultDeserializer + dataDeserializer = ref.dataDeserializer ) } @@ -114,7 +114,7 @@ internal constructor( revision = ref.revision, variables = variables, variablesSerializer = ref.variablesSerializer, - resultDeserializer = ref.resultDeserializer + dataDeserializer = ref.dataDeserializer ) } override fun close() { @@ -182,62 +182,62 @@ internal constructor( } } -inline fun FirebaseDataConnect.query( +inline fun FirebaseDataConnect.query( operationName: String, operationSet: String, revision: String -): QueryRef = +): QueryRef = query( operationName = operationName, operationSet = operationSet, revision = revision, variablesSerializer = serializer(), - resultDeserializer = serializer(), + dataDeserializer = serializer(), ) -fun FirebaseDataConnect.query( +fun FirebaseDataConnect.query( operationName: String, operationSet: String, revision: String, variablesSerializer: SerializationStrategy, - resultDeserializer: DeserializationStrategy -): QueryRef = + dataDeserializer: DeserializationStrategy +): QueryRef = QueryRef( dataConnect = this, operationName = operationName, operationSet = operationSet, revision = revision, variablesSerializer = variablesSerializer, - resultDeserializer = resultDeserializer + dataDeserializer = dataDeserializer ) -inline fun FirebaseDataConnect.mutation( +inline fun FirebaseDataConnect.mutation( operationName: String, operationSet: String, revision: String, -): MutationRef = +): MutationRef = mutation( operationName = operationName, operationSet = operationSet, revision = revision, variablesSerializer = serializer(), - resultDeserializer = serializer(), + dataDeserializer = serializer(), ) -fun FirebaseDataConnect.mutation( +fun FirebaseDataConnect.mutation( operationName: String, operationSet: String, revision: String, variablesSerializer: SerializationStrategy, - resultDeserializer: DeserializationStrategy -): MutationRef = + dataDeserializer: DeserializationStrategy +): MutationRef = MutationRef( dataConnect = this, operationName = operationName, operationSet = operationSet, revision = revision, variablesSerializer = variablesSerializer, - resultDeserializer = resultDeserializer + dataDeserializer = dataDeserializer ) open class DataConnectException internal constructor(message: String, cause: Throwable? = null) : @@ -248,6 +248,3 @@ open class NetworkTransportException internal constructor(message: String, cause open class GraphQLException internal constructor(message: String, val errors: List) : DataConnectException(message) - -open class ResultDecodeException internal constructor(message: String) : - DataConnectException(message) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt index b06ab701de2..34d7ed38383 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt @@ -16,23 +16,23 @@ package com.google.firebase.dataconnect import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy -class MutationRef +class MutationRef internal constructor( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, revision: String, variablesSerializer: SerializationStrategy, - resultDeserializer: DeserializationStrategy + dataDeserializer: DeserializationStrategy ) : - BaseRef( + BaseRef( dataConnect = dataConnect, operationName = operationName, operationSet = operationSet, revision = revision, variablesSerializer = variablesSerializer, - resultDeserializer = resultDeserializer, + dataDeserializer = dataDeserializer, ) { - override suspend fun execute(variables: VariablesType): ResultType = + override suspend fun execute(variables: VariablesType): DataType = dataConnect.executeMutation(this, variables) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt index 6557587c71e..c3d34a47b87 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt @@ -16,26 +16,26 @@ package com.google.firebase.dataconnect import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy -class QueryRef +class QueryRef internal constructor( dataConnect: FirebaseDataConnect, operationName: String, operationSet: String, revision: String, variablesSerializer: SerializationStrategy, - resultDeserializer: DeserializationStrategy + dataDeserializer: DeserializationStrategy ) : - BaseRef( + BaseRef( dataConnect = dataConnect, operationName = operationName, operationSet = operationSet, revision = revision, variablesSerializer = variablesSerializer, - resultDeserializer = resultDeserializer, + dataDeserializer = dataDeserializer, ) { - override suspend fun execute(variables: VariablesType): ResultType = + override suspend fun execute(variables: VariablesType): DataType = dataConnect.executeQuery(this, variables) - fun subscribe(variables: VariablesType): QuerySubscription = + fun subscribe(variables: VariablesType): QuerySubscription = QuerySubscription(this, variables) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt index f47e3c0ae65..c653a03c2a1 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt @@ -19,9 +19,9 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* -class QuerySubscription +class QuerySubscription internal constructor( - internal val query: QueryRef, + internal val query: QueryRef, variables: VariablesType ) { private val _variables = AtomicReference(variables) @@ -29,7 +29,7 @@ internal constructor( get() = _variables.get() private val sharedFlow = - MutableSharedFlow>( + MutableSharedFlow>( replay = 1, extraBufferCapacity = Integer.MAX_VALUE ) @@ -40,17 +40,17 @@ internal constructor( // NOTE: The variables below must ONLY be accessed from coroutines that use `sequentialDispatcher` // for their `CoroutineDispatcher`. Having this requirement removes the need for explicitly // synchronizing access to these variables. - private var inProgressReload: CompletableDeferred>? = null - private var pendingReload: CompletableDeferred>? = null + private var inProgressReload: CompletableDeferred>? = null + private var pendingReload: CompletableDeferred>? = null - val lastResult: Message? + val lastResult: Message? get() = sharedFlow.replayCache.firstOrNull() - fun reload(): Deferred> = + fun reload(): Deferred> = runBlocking(sequentialDispatcher) { pendingReload ?: run { - CompletableDeferred>().also { deferred -> + CompletableDeferred>().also { deferred -> if (inProgressReload == null) { inProgressReload = deferred query.dataConnect.coroutineScope.launch(sequentialDispatcher) { doReloadLoop() } @@ -66,7 +66,7 @@ internal constructor( reload() } - val flow: Flow> + val flow: Flow> get() = sharedFlow.asSharedFlow().onSubscription { reload() }.buffer(Channel.CONFLATED) private suspend fun doReloadLoop() { @@ -80,19 +80,16 @@ internal constructor( } } - class Message( - val variables: VariablesType, - val result: Result - ) + class Message(val variables: VariablesType, val data: Result) } -private suspend fun reload( +private suspend fun reload( variables: VariablesType, - query: QueryRef + query: QueryRef ) = QuerySubscription.Message( variables = variables, - result = + data = try { Result.success(query.execute(variables)) } catch (e: Throwable) { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index 52e908b4a88..2c3e1590bac 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -22,19 +22,19 @@ class FirebaseDataConnect { val queries: Queries = TODO() } -fun FirebaseDataConnect.query( +fun FirebaseDataConnect.query( operationName: String, operationSet: String, revision: String, variablesSerializer: SerializationStrategy, - resultDeserializer: DeserializationStrategy -): QueryRef = TODO() + dataDeserializer: DeserializationStrategy +): QueryRef = TODO() -inline fun FirebaseDataConnect.query( +inline fun FirebaseDataConnect.query( operationName: String, operationSet: String, revision: String -): QueryRef = TODO() +): QueryRef = TODO() open class DataConnectException internal constructor() : Exception() @@ -45,28 +45,22 @@ open class GraphQLException internal constructor() : DataConnectException() { get() = TODO() } -open class ResultDecodeException internal constructor() : DataConnectException() - -abstract class BaseRef internal constructor() { +abstract class BaseRef internal constructor() { val dataConnect: FirebaseDataConnect get() = TODO() - abstract suspend fun execute(variables: VariablesType): ResultType - - interface Codec { - fun decodeResult(map: Map): ResultType - } + abstract suspend fun execute(variables: VariablesType): DataType } -class QueryRef internal constructor() : - BaseRef() { - override suspend fun execute(variables: VariablesType): ResultType = TODO() +class QueryRef internal constructor() : + BaseRef() { + override suspend fun execute(variables: VariablesType): DataType = TODO() - fun subscribe(variables: VariablesType): QuerySubscription = TODO() + fun subscribe(variables: VariablesType): QuerySubscription = TODO() } -class QuerySubscription internal constructor() { - val query: QueryRef +class QuerySubscription internal constructor() { + val query: QueryRef get() = TODO() val variables: VariablesType get() = TODO() @@ -74,7 +68,7 @@ class QuerySubscription internal constructor() { // Alternative considered: add `lastResult`. The problem is, what do we do with this value if the // variables are changed via a call to update()? Do we clear it? Or do we leave it there even // though it came from a request with potentially-different variables? - val lastResult: Message? + val lastResult: Message? get() = TODO() // Alternative considered: Return `Deferred>` so that customer knows when the reload @@ -85,12 +79,9 @@ class QuerySubscription internal constructor() { // some previous call to reload() by some other unrelated operation. fun reload(): Unit = TODO() - val flow: Flow> = TODO() + val flow: Flow> = TODO() - class Message( - val variables: VariablesType, - val result: Result - ) + class Message(val variables: VariablesType, val data: Result) } //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -101,31 +92,31 @@ class GetPostQuery private constructor() { @Serializable data class Variables(val id: String) - data class Result(val post: Post?) { + data class Data(val post: Post?) { data class Post(val content: String, val comments: List) { data class Comment(val id: String, val content: String) } } companion object { - fun query(dataConnect: FirebaseDataConnect): QueryRef = TODO() + fun query(dataConnect: FirebaseDataConnect): QueryRef = TODO() } } -val FirebaseDataConnect.Queries.getPost: QueryRef +val FirebaseDataConnect.Queries.getPost: QueryRef get() = TODO() -suspend fun QueryRef.execute( +suspend fun QueryRef.execute( id: String -): GetPostQuery.Result = TODO() +): GetPostQuery.Data = TODO() -fun QueryRef.subscribe( +fun QueryRef.subscribe( id: String -): QuerySubscription = TODO() +): QuerySubscription = TODO() -typealias GetPostQueryRef = QueryRef +typealias GetPostQueryRef = QueryRef -typealias GetPostQuerySubscription = QuerySubscription +typealias GetPostQuerySubscription = QuerySubscription //////////////////////////////////////////////////////////////////////////////////////////////////// // CUSTOMER CODE @@ -145,10 +136,10 @@ private class MainActivity : Activity() { querySubscriptionFlow = activityCoroutineScope.launch { it.flow.collect { - if (it.result.isSuccess) { - showPostContent(it.variables.id, it.result.getOrThrow()) + if (it.data.isSuccess) { + showPostContent(it.variables.id, it.data.getOrThrow()) } else { - showError(it.variables.id, it.result.exceptionOrNull()!!) + showError(it.variables.id, it.data.exceptionOrNull()!!) } } } @@ -178,5 +169,5 @@ private class MainActivity : Activity() { fun getIdFromTextView(): String = TODO() fun showError(postId: String, exception: Throwable): Unit = TODO() - fun showPostContent(postId: String, post: GetPostQuery.Result?): Unit = TODO() + fun showPostContent(postId: String, post: GetPostQuery.Data?): Unit = TODO() } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt index 1401a4e5794..8a475b00791 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -38,7 +38,7 @@ class GetPostQuery private constructor() { } @Serializable - data class Result(val post: Post?) { + data class Data(val post: Post?) { @Serializable data class Post(val content: String, val comments: List) { @Serializable data class Comment(val id: String, val content: String) @@ -48,7 +48,7 @@ class GetPostQuery private constructor() { companion object { fun query(dataConnect: FirebaseDataConnect) = - dataConnect.query( + dataConnect.query( operationName = "getPost", operationSet = "crud", revision = "1234567890abcdef", @@ -56,18 +56,18 @@ class GetPostQuery private constructor() { } } -typealias GetPostQuerySubscription = QuerySubscription +typealias GetPostQuerySubscription = QuerySubscription val FirebaseDataConnect.Queries.getPost get() = GetPostQuery.query(dataConnect) -suspend fun QueryRef.execute(id: String) = +suspend fun QueryRef.execute(id: String) = execute(variablesFor(id = id)) -fun QueryRef.subscribe(id: String) = +fun QueryRef.subscribe(id: String) = subscribe(variablesFor(id = id)) -fun QuerySubscription.update( +fun QuerySubscription.update( block: GetPostQuery.Variables.Builder.() -> Unit ) = update(variables.build(block)) From c2d89b6a18ae0fd1dd94cb1c49f0a8b344af1edb Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 20 Nov 2023 13:18:20 -0500 Subject: [PATCH 068/573] DataConnectResult.kt added (but not used yet) --- .../firebase/dataconnect/DataConnectResult.kt | 118 +++++++ .../apiproposal/QueryApiProposal.kt | 32 ++ .../dataconnect/DataConnectErrorTest.kt | 312 +++++++++++++++++ .../dataconnect/DataConnectResultTest.kt | 317 ++++++++++++++++++ .../dataconnect/PathSegmentFieldTest.kt | 58 ++++ .../dataconnect/PathSegmentListIndexTest.kt | 58 ++++ .../google/firebase/dataconnect/TestUtils.kt | 13 + 7 files changed, 908 insertions(+) create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/TestUtils.kt diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt new file mode 100644 index 00000000000..07976bf4bb2 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt @@ -0,0 +1,118 @@ +// Copyright 2023 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.dataconnect + +class DataConnectResult +private constructor(private val impl: Impl) { + + internal constructor( + variables: VariablesType, + data: DataType?, + errors: List + ) : this(Impl(variables = variables, data = data, errors = errors)) + + val variables: VariablesType + get() = impl.variables + val data: DataType? + get() = impl.data + val errors: List + get() = impl.errors + + override fun hashCode() = impl.hashCode() + override fun equals(other: Any?) = + (other as? DataConnectResult<*, *>)?.let { it.impl == impl } ?: false + override fun toString() = impl.toString() + + private data class Impl( + val variables: VariablesType, + val data: DataType?, + val errors: List, + ) +} + +// See https://spec.graphql.org/October2021/#sec-Errors +class DataConnectError private constructor(private val impl: Impl) { + + internal constructor( + message: String, + path: List, + extensions: Map + ) : this(Impl(message = message, path = path, extensions = extensions)) + + val message: String + get() = impl.message + val path: List + get() = impl.path + val extensions: Map + get() = impl.extensions + + override fun hashCode() = impl.hashCode() + override fun equals(other: Any?) = (other as? DataConnectError)?.let { it.impl == impl } ?: false + + override fun toString(): String { + val sb = StringBuilder() + + if (path.isNotEmpty()) { + path.forEachIndexed { segmentIndex, segment -> + when (segment) { + is PathSegment.Field -> { + if (segmentIndex != 0) { + sb.append('.') + } + sb.append(segment.field) + } + is PathSegment.ListIndex -> { + sb.append('[') + sb.append(segment.index) + sb.append(']') + } + } + } + sb.append(": ") + } + + sb.append(message) + + if (extensions.isNotEmpty()) { + sb.append(" (") + extensions.keys.sorted().forEachIndexed { keyIndex, key -> + if (keyIndex != 0) { + sb.append(", ") + } + sb.append(key).append('=').append(extensions[key]) + } + sb.append(')') + } + + return sb.toString() + } + + sealed interface PathSegment { + @JvmInline + value class Field(val field: String) : PathSegment { + override fun toString() = field + } + + @JvmInline + value class ListIndex(val index: Int) : PathSegment { + override fun toString() = index.toString() + } + } + + private data class Impl( + val message: String, + val path: List, + val extensions: Map, + ) +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt index 2c3e1590bac..6d7a814a832 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/apiproposal/QueryApiProposal.kt @@ -45,6 +45,38 @@ open class GraphQLException internal constructor() : DataConnectException() { get() = TODO() } +class DataConnectResult private constructor() { + val variables: VariablesType + get() = TODO() + val data: DataType? + get() = TODO() + val errors: List + get() = TODO() + + override fun hashCode() = TODO() + override fun equals(other: Any?) = TODO() + override fun toString() = TODO() +} + +// See https://spec.graphql.org/October2021/#sec-Errors +class DataConnectError private constructor() { + val message: String + get() = TODO() + val path: List + get() = TODO() + val extensions: Map + get() = TODO() + + override fun hashCode() = TODO() + override fun equals(other: Any?) = TODO() + override fun toString(): String = TODO() + + sealed interface PathSegment { + @JvmInline value class Field(val field: String) : PathSegment + @JvmInline value class ListIndex(val index: Int) : PathSegment + } +} + abstract class BaseRef internal constructor() { val dataConnect: FirebaseDataConnect get() = TODO() diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorTest.kt new file mode 100644 index 00000000000..eadf8145b86 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorTest.kt @@ -0,0 +1,312 @@ +package com.google.firebase.dataconnect + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.DataConnectError.PathSegment +import org.junit.Test + +class DataConnectErrorTest { + + @Test + fun `message should be the same object given to the constructor`() { + val message = "This is the test message" + val dataConnectError = + DataConnectError(message = message, path = SAMPLE_PATH, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError.message).isSameInstanceAs(message) + } + + @Test + fun `path should be the same object given to the constructor`() { + val path = listOf(PathSegment.Field("foo"), PathSegment.ListIndex(42)) + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = path, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError.path).isSameInstanceAs(path) + } + + @Test + fun `extensions should be the same object given to the constructor`() { + val extensions = mapOf("foo" to 42, "bar" to "BAR") + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = SAMPLE_PATH, extensions = extensions) + assertThat(dataConnectError.extensions).isSameInstanceAs(extensions) + } + + @Test + fun `toString() should incorporate the message`() { + val message = "This is the test message" + val dataConnectError = + DataConnectError(message = message, path = SAMPLE_PATH, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError.toString()).containsWithNonAdjacentText(message) + } + + @Test + fun `toString() should incorporate the fields from the path separated by dots`() { + val path = listOf(PathSegment.Field("foo"), PathSegment.Field("bar"), PathSegment.Field("baz")) + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = path, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError.toString()).containsWithNonAdjacentText("foo.bar.baz") + } + + @Test + fun `toString() should incorporate the list indexes from the path surround by square brackets`() { + val path = + listOf(PathSegment.ListIndex(42), PathSegment.ListIndex(99), PathSegment.ListIndex(1)) + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = path, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError.toString()).containsWithNonAdjacentText("[42][99][1]") + } + + @Test + fun `toString() should incorporate the fields and list indexes from the path`() { + val path = + listOf( + PathSegment.Field("foo"), + PathSegment.ListIndex(99), + PathSegment.Field("bar"), + PathSegment.ListIndex(22), + PathSegment.ListIndex(33) + ) + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = path, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError.toString()).containsWithNonAdjacentText("foo[99].bar[22][33]") + } + + @Test + fun `toString() should incorporate the extensions`() { + val extensions = mapOf("foo" to 42, "bar" to "zzyzx") + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = SAMPLE_PATH, extensions = extensions) + assertThat(dataConnectError.toString()).containsWithNonAdjacentText("foo=42") + assertThat(dataConnectError.toString()).containsWithNonAdjacentText("bar=zzyzx") + } + + @Test + fun `toString() should sort the extensions by key, lexicographically`() { + val extensions = mapOf("bbb" to "zzyzx", "ccc" to false, "aaa" to 42) + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = SAMPLE_PATH, extensions = extensions) + assertThat(dataConnectError.toString()).containsMatch("aaa=.*bbb=.*ccc=.*") + } + + @Test + fun `equals() should return true for the exact same instance`() { + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = SAMPLE_PATH, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError.equals(dataConnectError)).isTrue() + } + + @Test + fun `equals() should return true for an equal instance`() { + val dataConnectError1 = + DataConnectError( + message = "Test Message", + path = listOf(PathSegment.Field("foo"), PathSegment.ListIndex(42)), + extensions = mapOf("foo" to 42, "bar" to "BAR") + ) + val dataConnectError2 = + DataConnectError( + message = "Test Message", + path = listOf(PathSegment.Field("foo"), PathSegment.ListIndex(42)), + extensions = mapOf("foo" to 42, "bar" to "BAR") + ) + assertThat(dataConnectError1.equals(dataConnectError2)).isTrue() + } + + @Test + fun `equals() should return false for null`() { + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = SAMPLE_PATH, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError.equals(null)).isFalse() + } + + @Test + fun `equals() should return false for a different type`() { + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = SAMPLE_PATH, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError.equals(listOf("foo"))).isFalse() + } + + @Test + fun `equals() should return false when only message differs`() { + val dataConnectError1 = + DataConnectError( + message = "Test Message1", + path = SAMPLE_PATH, + extensions = SAMPLE_EXTENSIONS + ) + val dataConnectError2 = + DataConnectError( + message = "Test Message2", + path = SAMPLE_PATH, + extensions = SAMPLE_EXTENSIONS + ) + assertThat(dataConnectError1.equals(dataConnectError2)).isFalse() + } + + @Test + fun `equals() should return false when message differs only in character case`() { + val dataConnectError1 = + DataConnectError(message = "A", path = SAMPLE_PATH, extensions = SAMPLE_EXTENSIONS) + val dataConnectError2 = + DataConnectError(message = "a", path = SAMPLE_PATH, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError1.equals(dataConnectError2)).isFalse() + } + + @Test + fun `equals() should return false when path differs, with field`() { + val dataConnectError1 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = listOf(PathSegment.Field("a")), + extensions = SAMPLE_EXTENSIONS + ) + val dataConnectError2 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = listOf(PathSegment.Field("z")), + extensions = SAMPLE_EXTENSIONS + ) + assertThat(dataConnectError1.equals(dataConnectError2)).isFalse() + } + + @Test + fun `equals() should return false when path differs, with list index`() { + val dataConnectError1 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = listOf(PathSegment.ListIndex(1)), + extensions = SAMPLE_EXTENSIONS + ) + val dataConnectError2 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = listOf(PathSegment.ListIndex(2)), + extensions = SAMPLE_EXTENSIONS + ) + assertThat(dataConnectError1.equals(dataConnectError2)).isFalse() + } + + @Test + fun `equals() should return false when path differs, with field and list index`() { + val dataConnectError1 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = listOf(PathSegment.ListIndex(1)), + extensions = SAMPLE_EXTENSIONS + ) + val dataConnectError2 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = listOf(PathSegment.Field("foo")), + extensions = SAMPLE_EXTENSIONS + ) + assertThat(dataConnectError1.equals(dataConnectError2)).isFalse() + } + + @Test + fun `equals() should return false when extensions differs in values only`() { + val dataConnectError1 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = SAMPLE_PATH, + extensions = mapOf("foo" to 42) + ) + val dataConnectError2 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = SAMPLE_PATH, + extensions = mapOf("foo" to 43) + ) + assertThat(dataConnectError1.equals(dataConnectError2)).isFalse() + } + + @Test + fun `equals() should return false when extensions differs in keys only`() { + val dataConnectError1 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = SAMPLE_PATH, + extensions = mapOf("foo" to 42) + ) + val dataConnectError2 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = SAMPLE_PATH, + extensions = mapOf("bar" to 42) + ) + assertThat(dataConnectError1.equals(dataConnectError2)).isFalse() + } + + @Test + fun `hashCode() should return the same value each time it is invoked on a given object`() { + val dataConnectError = + DataConnectError(message = SAMPLE_MESSAGE, path = SAMPLE_PATH, extensions = SAMPLE_EXTENSIONS) + val hashCode = dataConnectError.hashCode() + assertThat(dataConnectError.hashCode()).isEqualTo(hashCode) + assertThat(dataConnectError.hashCode()).isEqualTo(hashCode) + assertThat(dataConnectError.hashCode()).isEqualTo(hashCode) + } + + @Test + fun `hashCode() should return the same value on equal objects`() { + val dataConnectError1 = + DataConnectError(message = SAMPLE_MESSAGE, path = SAMPLE_PATH, extensions = SAMPLE_EXTENSIONS) + val dataConnectError2 = + DataConnectError(message = SAMPLE_MESSAGE, path = SAMPLE_PATH, extensions = SAMPLE_EXTENSIONS) + assertThat(dataConnectError1.hashCode()).isEqualTo(dataConnectError2.hashCode()) + } + + @Test + fun `hashCode() should return a different value if message is different`() { + val dataConnectError1 = + DataConnectError( + message = "Test Message 1", + path = SAMPLE_PATH, + extensions = SAMPLE_EXTENSIONS + ) + val dataConnectError2 = + DataConnectError( + message = "Test Message 2", + path = SAMPLE_PATH, + extensions = SAMPLE_EXTENSIONS + ) + assertThat(dataConnectError1.hashCode()).isNotEqualTo(dataConnectError2.hashCode()) + } + + @Test + fun `hashCode() should return a different value if path is different`() { + val dataConnectError1 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = listOf(PathSegment.Field("foo")), + extensions = SAMPLE_EXTENSIONS + ) + val dataConnectError2 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = listOf(PathSegment.ListIndex(42)), + extensions = SAMPLE_EXTENSIONS + ) + assertThat(dataConnectError1.hashCode()).isNotEqualTo(dataConnectError2.hashCode()) + } + + @Test + fun `hashCode() should return a different value if extensions is different`() { + val dataConnectError1 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = SAMPLE_PATH, + extensions = mapOf("foo" to 42) + ) + val dataConnectError2 = + DataConnectError( + message = SAMPLE_MESSAGE, + path = SAMPLE_PATH, + extensions = mapOf("bar" to 24) + ) + assertThat(dataConnectError1.hashCode()).isNotEqualTo(dataConnectError2.hashCode()) + } +} + +private val SAMPLE_MESSAGE = "This is a sample message" +private val SAMPLE_PATH = listOf(PathSegment.Field("foo"), PathSegment.ListIndex(42)) +private val SAMPLE_EXTENSIONS = mapOf("foo" to 42, "bar" to "BAR") diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt new file mode 100644 index 00000000000..d5161f2b163 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt @@ -0,0 +1,317 @@ +package com.google.firebase.dataconnect + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.DataConnectError.PathSegment +import org.junit.Test + +class DataConnectResultTest { + + @Test + fun `variables should be the same object given to the constructor`() { + val variables = TestVariables("boo") + val dataConnectResult = + DataConnectResult(variables = variables, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult.variables).isSameInstanceAs(variables) + } + + @Test + fun `data should be the same object given to the constructor`() { + val data = TestData("blah") + val dataConnectResult = + DataConnectResult(variables = SAMPLE_VARIABLES, data = data, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult.data).isSameInstanceAs(data) + } + + @Test + fun `data should be null if null was given to the constructor`() { + val dataConnectResult = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = null, + errors = SAMPLE_ERRORS + ) + assertThat(dataConnectResult.data).isNull() + } + + @Test + fun `errors should be the same object given to the constructor`() { + val errors = listOf(SAMPLE_ERROR1) + val dataConnectResult = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = errors) + assertThat(dataConnectResult.errors).isSameInstanceAs(errors) + } + + @Test + fun `errors should be empty if empty was given to the constructor`() { + val dataConnectResult = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = null, + errors = emptyList() + ) + assertThat(dataConnectResult.errors).isEmpty() + } + + @Test + fun `toString() should incorporate the variables`() { + val variables = + object { + override fun toString() = "TestVariablesToString" + } + val dataConnectResult = + DataConnectResult(variables = variables, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult.toString()).containsWithNonAdjacentText("TestVariablesToString") + } + + @Test + fun `toString() should incorporate the data`() { + val data = + object { + override fun toString() = "TestDataToString" + } + val dataConnectResult = + DataConnectResult(variables = SAMPLE_VARIABLES, data = data, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult.toString()).containsWithNonAdjacentText("TestDataToString") + } + + @Test + fun `toString() should incorporate the data, if null`() { + val dataConnectResult = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = null, + errors = emptyList() + ) + assertThat(dataConnectResult.toString()).containsWithNonAdjacentText("null") + } + + @Test + fun `toString() should incorporate the errors`() { + val errors = listOf(SAMPLE_ERROR1, SAMPLE_ERROR2) + val dataConnectResult = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = errors) + assertThat(dataConnectResult.toString()).containsWithNonAdjacentText(errors.toString()) + } + + @Test + fun `toString() should incorporate the errors, if empty`() { + val dataConnectResult = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = emptyList()) + assertThat(dataConnectResult.toString()) + .containsWithNonAdjacentText(emptyList().toString()) + } + + @Test + fun `equals() should return true for the exact same instance`() { + val dataConnectResult = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult.equals(dataConnectResult)).isTrue() + } + + @Test + fun `equals() should return true for an equal instance`() { + val dataConnectResult1 = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + val dataConnectResult2 = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult1.equals(dataConnectResult2)).isTrue() + } + + @Test + fun `equals() should return true if all properties are equal, and data is null`() { + val dataConnectResult1 = + DataConnectResult(variables = SAMPLE_VARIABLES, data = null, errors = SAMPLE_ERRORS) + val dataConnectResult2 = + DataConnectResult(variables = SAMPLE_VARIABLES, data = null, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult1.equals(dataConnectResult2)).isTrue() + } + + @Test + fun `equals() should return false for null`() { + val dataConnectResult = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult.equals(null)).isFalse() + } + + @Test + fun `equals() should return false for a different type`() { + val dataConnectResult = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult.equals(listOf("foo"))).isFalse() + } + + @Test + fun `equals() should return false when only variables differs`() { + val dataConnectResult1 = + DataConnectResult( + variables = TestVariables("foo"), + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS + ) + val dataConnectResult2 = + DataConnectResult( + variables = TestVariables("bar"), + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS + ) + assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() + } + + @Test + fun `equals() should return false when only data differs`() { + val dataConnectResult1 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = TestData("foo"), + errors = SAMPLE_ERRORS + ) + val dataConnectResult2 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = TestData("bar"), + errors = SAMPLE_ERRORS + ) + assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() + } + + @Test + fun `equals() should return false when data of first object is null`() { + val dataConnectResult1 = + DataConnectResult(variables = SAMPLE_VARIABLES, data = null, errors = SAMPLE_ERRORS) + val dataConnectResult2 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = TestData("bar"), + errors = SAMPLE_ERRORS + ) + assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() + } + + @Test + fun `equals() should return false when data of second object is null`() { + val dataConnectResult1 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = TestData("foo"), + errors = SAMPLE_ERRORS + ) + val dataConnectResult2 = + DataConnectResult(variables = SAMPLE_VARIABLES, data = null, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() + } + + @Test + fun `equals() should return false when only errors differs`() { + val dataConnectResult1 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = listOf(SAMPLE_ERROR1) + ) + val dataConnectResult2 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = listOf(SAMPLE_ERROR2) + ) + assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() + } + + @Test + fun `hashCode() should return the same value each time it is invoked on a given object`() { + val dataConnectResult = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + val hashCode = dataConnectResult.hashCode() + assertThat(dataConnectResult.hashCode()).isEqualTo(hashCode) + assertThat(dataConnectResult.hashCode()).isEqualTo(hashCode) + assertThat(dataConnectResult.hashCode()).isEqualTo(hashCode) + } + + @Test + fun `hashCode() should return the same value on equal objects`() { + val dataConnectResult1 = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + val dataConnectResult2 = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult1.hashCode()).isEqualTo(dataConnectResult2.hashCode()) + } + + @Test + fun `hashCode() should return a different value if variables is different`() { + val dataConnectResult1 = + DataConnectResult( + variables = TestVariables("foo"), + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS + ) + val dataConnectResult2 = + DataConnectResult( + variables = TestVariables("bar"), + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS + ) + assertThat(dataConnectResult1.hashCode()).isNotEqualTo(dataConnectResult2.hashCode()) + } + + @Test + fun `hashCode() should return a different value if data is different`() { + val dataConnectResult1 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = TestData("foo"), + errors = SAMPLE_ERRORS + ) + val dataConnectResult2 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = TestData("bar"), + errors = SAMPLE_ERRORS + ) + assertThat(dataConnectResult1.hashCode()).isNotEqualTo(dataConnectResult2.hashCode()) + } + + @Test + fun `hashCode() should return a different value if errors is different`() { + val dataConnectResult1 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = listOf(SAMPLE_ERROR1) + ) + val dataConnectResult2 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = listOf(SAMPLE_ERROR2) + ) + assertThat(dataConnectResult1.hashCode()).isNotEqualTo(dataConnectResult2.hashCode()) + } +} + +private data class TestVariables(val value: String) + +private val SAMPLE_VARIABLES = TestVariables("foo") + +private data class TestData(val value: String) + +private val SAMPLE_DATA = TestData("bar") + +private val SAMPLE_ERROR_MESSAGE1 = "This is a sample message" +private val SAMPLE_ERROR_PATH1 = listOf(PathSegment.Field("foo"), PathSegment.ListIndex(42)) +private val SAMPLE_ERROR_EXTENSIONS1 = mapOf("foo" to 42, "bar" to "BAR") +private val SAMPLE_ERROR_MESSAGE2 = "This is a sample message 2" +private val SAMPLE_ERROR_PATH2 = listOf(PathSegment.Field("bar"), PathSegment.ListIndex(24)) +private val SAMPLE_ERROR_EXTENSIONS2 = mapOf("blah" to 99, "zzz" to "foo") +private val SAMPLE_ERROR1 = + DataConnectError( + message = SAMPLE_ERROR_MESSAGE1, + path = SAMPLE_ERROR_PATH1, + extensions = SAMPLE_ERROR_EXTENSIONS1 + ) +private val SAMPLE_ERROR2 = + DataConnectError( + message = SAMPLE_ERROR_MESSAGE2, + path = SAMPLE_ERROR_PATH2, + extensions = SAMPLE_ERROR_EXTENSIONS2 + ) +private val SAMPLE_ERRORS = listOf(SAMPLE_ERROR1, SAMPLE_ERROR2) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldTest.kt new file mode 100644 index 00000000000..0d9cfc38138 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldTest.kt @@ -0,0 +1,58 @@ +package com.google.firebase.dataconnect + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.DataConnectError.PathSegment +import org.junit.Test + +class PathSegmentFieldTest { + + @Test + fun `field should equal the value given to the constructor`() { + val segment = PathSegment.Field("foo") + assertThat(segment.field).isEqualTo("foo") + } + + @Test + fun `toString() should equal the field`() { + val segment = PathSegment.Field("foo") + assertThat(segment.toString()).isEqualTo("foo") + } + + @Test + fun `equals() should return true for the same instance`() { + val segment = PathSegment.Field("foo") + assertThat(segment.equals(segment)).isTrue() + } + + @Test + fun `equals() should return true for an equal field`() { + val segment1 = PathSegment.Field("foo") + val segment2 = PathSegment.Field("foo") + assertThat(segment1.equals(segment2)).isTrue() + } + + @Test + fun `equals() should return false for null`() { + val segment = PathSegment.Field("foo") + assertThat(segment.equals(null)).isFalse() + } + + @Test + fun `equals() should return false for a different type`() { + val segment = PathSegment.Field("foo") + assertThat(segment.equals(listOf("foo"))).isFalse() + } + + @Test + fun `equals() should return false for a different field`() { + val segment1 = PathSegment.Field("foo") + val segment2 = PathSegment.Field("bar") + assertThat(segment1.equals(segment2)).isFalse() + } + + @Test + fun `hashCode() should return the same value as the field's hashCode() method`() { + assertThat(PathSegment.Field("foo").hashCode()).isEqualTo("foo".hashCode()) + assertThat(PathSegment.Field("bar").hashCode()).isEqualTo("bar".hashCode()) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexTest.kt new file mode 100644 index 00000000000..0a038207153 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexTest.kt @@ -0,0 +1,58 @@ +package com.google.firebase.dataconnect + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.DataConnectError.PathSegment +import org.junit.Test + +class PathSegmentListIndexTest { + + @Test + fun `index should equal the value given to the constructor`() { + val segment = PathSegment.ListIndex(42) + assertThat(segment.index).isEqualTo(42) + } + + @Test + fun `toString() should equal the field`() { + val segment = PathSegment.ListIndex(42) + assertThat(segment.toString()).isEqualTo("42") + } + + @Test + fun `equals() should return true for the same instance`() { + val segment = PathSegment.ListIndex(42) + assertThat(segment.equals(segment)).isTrue() + } + + @Test + fun `equals() should return true for an equal field`() { + val segment1 = PathSegment.ListIndex(42) + val segment2 = PathSegment.ListIndex(42) + assertThat(segment1.equals(segment2)).isTrue() + } + + @Test + fun `equals() should return false for null`() { + val segment = PathSegment.ListIndex(42) + assertThat(segment.equals(null)).isFalse() + } + + @Test + fun `equals() should return false for a different type`() { + val segment = PathSegment.ListIndex(42) + assertThat(segment.equals(listOf("foo"))).isFalse() + } + + @Test + fun `equals() should return false for a different index`() { + val segment1 = PathSegment.ListIndex(42) + val segment2 = PathSegment.ListIndex(43) + assertThat(segment1.equals(segment2)).isFalse() + } + + @Test + fun `hashCode() should return the same value as the field's hashCode() method`() { + assertThat(PathSegment.ListIndex(42).hashCode()).isEqualTo(42.hashCode()) + assertThat(PathSegment.ListIndex(43).hashCode()).isEqualTo(43.hashCode()) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/TestUtils.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/TestUtils.kt new file mode 100644 index 00000000000..c1c2080c04b --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/TestUtils.kt @@ -0,0 +1,13 @@ +package com.google.firebase.dataconnect + +import com.google.common.truth.StringSubject +import java.util.regex.Pattern + +/** + * Asserts that a string contains another string, verifying that the character immediately preceding + * the text, if any, is a non-word character, and that the character immediately following the text, + * if any, is also a non-word character. This effectively verifies that the given string is included + * in the string being checked without being "mashed" into adjacent text. + */ +fun StringSubject.containsWithNonAdjacentText(text: String) = + containsMatch("(^|\\W)${Pattern.quote(text)}($|\\W)") From 4859d171dcf0c29e5bea401f017a23d567385e42 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 22 Nov 2023 15:34:48 -0500 Subject: [PATCH 069/573] Use DataConnectResult throughout --- .../dataconnect/QuerySubscriptionTest.kt | 44 +++++++------ .../dataconnect/generated/PostsTest.kt | 16 ++--- .../testutil/schemas/PersonSchemaTest.kt | 30 ++++----- .../google/firebase/dataconnect/BaseRef.kt | 2 +- .../dataconnect/DataConnectGrpcClient.kt | 63 ++++++++++--------- .../firebase/dataconnect/DataConnectResult.kt | 6 +- .../dataconnect/FirebaseDataConnect.kt | 11 +++- .../firebase/dataconnect/MutationRef.kt | 5 +- .../google/firebase/dataconnect/QueryRef.kt | 5 +- .../firebase/dataconnect/QuerySubscription.kt | 37 ++++------- 10 files changed, 107 insertions(+), 112 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt index 16cd2373276..7e518aa6384 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt @@ -71,19 +71,22 @@ class QuerySubscriptionTest { val querySubscription = schema.getPerson.subscribe(id = "TestId12345") withTimeout(2.seconds) { - val resultsChannel = Channel(capacity = Channel.UNLIMITED) - val collectJob = launch { - querySubscription.flow.collect { resultsChannel.send(it.data.getOrThrow()) } - } + val resultsChannel = + Channel< + DataConnectResult + >( + capacity = Channel.UNLIMITED + ) + val collectJob = launch { querySubscription.flow.collect(resultsChannel::send) } val result1 = resultsChannel.receive() - assertThat(result1).isEqualToGetPersonQueryResult(name = "Name0", age = 10000) + assertThat(result1.data.person).isEqualToGetPersonQueryResult(name = "Name0", age = 10000) schema.updatePerson.execute(id = "TestId12345", name = "Name1", age = 10001) querySubscription.reload() val result2 = resultsChannel.receive() - assertThat(result2).isEqualToGetPersonQueryResult(name = "Name1", age = 10001) + assertThat(result2.data.person).isEqualToGetPersonQueryResult(name = "Name1", age = 10001) collectJob.cancel() } @@ -95,12 +98,12 @@ class QuerySubscriptionTest { val querySubscription = schema.getPerson.subscribe(id = "TestId12345") withTimeout(2.seconds) { - val result1 = querySubscription.flow.first().data.getOrThrow() + val result1 = querySubscription.flow.first().data.person assertThat(result1).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) schema.updatePerson.execute(id = "TestId12345", name = "TestName2", age = 10002) - val result2 = querySubscription.flow.first().data.getOrThrow() + val result2 = querySubscription.flow.first().data.person assertThat(result2).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) } } @@ -117,7 +120,7 @@ class QuerySubscriptionTest { repeat(5) { assertWithMessage("fast flow retrieval iteration $it") - .that(querySubscription.flow.first().data.getOrThrow()) + .that(querySubscription.flow.first().data.person) .isEqualToGetPersonQueryResult(name = "TestName", age = 10000) } @@ -133,25 +136,25 @@ class QuerySubscriptionTest { withTimeout(2.seconds) { val resultsChannel1 = Channel(capacity = Channel.UNLIMITED) val flowJob1 = launch { - querySubscription.flow.collect { resultsChannel1.send(it.data.getOrThrow()) } + querySubscription.flow.map { it.data }.collect(resultsChannel1::send) } val resultsChannel2 = Channel(capacity = Channel.UNLIMITED) val flowJob2 = launch { - querySubscription.flow.collect { resultsChannel2.send(it.data.getOrThrow()) } + querySubscription.flow.map { it.data }.collect(resultsChannel2::send) } resultsChannel1.purge(0.25.seconds).forEach { - assertThat(it).isEqualToGetPersonQueryResult(name = "TestName0", age = 10000) + assertThat(it.person).isEqualToGetPersonQueryResult(name = "TestName0", age = 10000) } resultsChannel2.purge(0.25.seconds).forEach { - assertThat(it).isEqualToGetPersonQueryResult(name = "TestName0", age = 10000) + assertThat(it.person).isEqualToGetPersonQueryResult(name = "TestName0", age = 10000) } schema.updatePerson.execute(id = "TestId12345", name = "TestName1", age = 10001) querySubscription.reload() - assertThat(resultsChannel1.receive()) + assertThat(resultsChannel1.receive().person) .isEqualToGetPersonQueryResult(name = "TestName1", age = 10001) - assertThat(resultsChannel2.receive()) + assertThat(resultsChannel2.receive().person) .isEqualToGetPersonQueryResult(name = "TestName1", age = 10001) flowJob1.cancel() @@ -165,8 +168,7 @@ class QuerySubscriptionTest { val querySubscription = schema.getPerson.subscribe(id = "TestId12345") withTimeout(5.seconds) { - val resultsChannel = - Channel>(capacity = Channel.UNLIMITED) + val resultsChannel = Channel(capacity = Channel.UNLIMITED) val collectJob = launch { querySubscription.flow.map { it.data }.collect(resultsChannel::send) } @@ -183,7 +185,7 @@ class QuerySubscriptionTest { while (true) { resultCount++ val result = withTimeoutOrNull(1.seconds) { resultsChannel.receive() } ?: break - assertThat(result.getOrThrow()).isEqualToGetPersonQueryResult(name = "Name", age = 10000) + assertThat(result.person).isEqualToGetPersonQueryResult(name = "Name", age = 10000) } assertThat(resultCount).isGreaterThan(0) @@ -193,11 +195,7 @@ class QuerySubscriptionTest { } private fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = - isEqualTo( - PersonSchema.GetPersonQuery.Data( - PersonSchema.GetPersonQuery.Data.Person(name = name, age = age) - ) - ) + isEqualTo(PersonSchema.GetPersonQuery.Data.Person(name = name, age = age)) private suspend fun ReceiveChannel.purge(timeout: Duration): List = coroutineScope { mutableListOf() diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt index 5f3e4b8ba02..f9d0992b1cd 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt @@ -45,7 +45,7 @@ class PostsTest { val dc = dataConnectFactory.newInstance(service = "local") runBlocking { val queryResponse = dc.queries.getPost.execute(id = UUID.randomUUID().toString()) - assertWithMessage("queryResponse").that(queryResponse.post).isNull() + assertWithMessage("queryResponse").that(queryResponse.data.post).isNull() } } @@ -61,7 +61,7 @@ class PostsTest { val queryResponse = dc.queries.getPost.execute(id = postId) assertWithMessage("queryResponse") - .that(queryResponse.post) + .that(queryResponse.data.post) .isEqualTo(GetPostQuery.Data.Post(content = postContent, comments = emptyList())) } } @@ -83,9 +83,9 @@ class PostsTest { assertWithMessage("lastResult 0").that(querySubscription.lastResult).isNull() val result1 = querySubscription.flow.timeout(5.seconds).first() - assertWithMessage("result1.isSuccess").that(result1.data.isSuccess).isTrue() + assertWithMessage("result1.isSuccess").that(result1.errors).isEmpty() assertWithMessage("result1.post.content") - .that(result1.data.getOrThrow().post?.content) + .that(result1.data.post?.content) .isEqualTo(postContent1) assertWithMessage("lastResult 1").that(querySubscription.lastResult).isEqualTo(result1) @@ -96,13 +96,13 @@ class PostsTest { val results2 = flow2Job.await() assertWithMessage("results2.size").that(results2.size).isEqualTo(2) - assertWithMessage("results2[0].isSuccess").that(results2[0].data.isSuccess).isTrue() - assertWithMessage("results2[1].isSuccess").that(results2[1].data.isSuccess).isTrue() + assertWithMessage("results2[0].isSuccess").that(results2[0].errors).isEmpty() + assertWithMessage("results2[1].isSuccess").that(results2[1].errors).isEmpty() assertWithMessage("results2[0].post.content") - .that(results2[0].data.getOrThrow().post?.content) + .that(results2[0].data.post?.content) .isEqualTo(postContent1) assertWithMessage("results2[1].post.content") - .that(results2[1].data.getOrThrow().post?.content) + .that(results2[1].data.post?.content) .isEqualTo(postContent2) assertWithMessage("lastResult 2").that(querySubscription.lastResult).isEqualTo(results2[1]) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt index f94c9f64f0b..7c0602a1c5b 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt @@ -35,18 +35,18 @@ class PersonSchemaTest { val result = schema.getPerson.execute(id = "1234") - assertThat(result.person?.name).isEqualTo("TestName") - assertThat(result.person?.age).isEqualTo(42) + assertThat(result.data.person?.name).isEqualTo("TestName") + assertThat(result.data.person?.age).isEqualTo(42) } @Test fun deletePersonShouldDeleteTheSpecifiedPerson(): Unit = runBlocking { schema.createPerson.execute(id = "1234", name = "TestName", age = 42) - assertThat(schema.getPerson.execute(id = "1234").person).isNotNull() + assertThat(schema.getPerson.execute(id = "1234").data.person).isNotNull() schema.deletePerson.execute(id = "1234") - assertThat(schema.getPerson.execute(id = "1234").person).isNull() + assertThat(schema.getPerson.execute(id = "1234").data.person).isNull() } @Test @@ -56,8 +56,8 @@ class PersonSchemaTest { schema.updatePerson.execute(id = "1234", name = "TestName99", age = 999) val result = schema.getPerson.execute(id = "1234") - assertThat(result.person?.name).isEqualTo("TestName99") - assertThat(result.person?.age).isEqualTo(999) + assertThat(result.data.person?.name).isEqualTo("TestName99") + assertThat(result.data.person?.age).isEqualTo(999) } @Test @@ -70,12 +70,12 @@ class PersonSchemaTest { val result2 = schema.getPerson.execute(id = "222") val result3 = schema.getPerson.execute(id = "333") - assertThat(result1.person?.name).isEqualTo("Name111") - assertThat(result1.person?.age).isEqualTo(111) - assertThat(result2.person?.name).isEqualTo("Name222") - assertThat(result2.person?.age).isEqualTo(222) - assertThat(result3.person?.name).isEqualTo("Name333") - assertThat(result3.person?.age).isNull() + assertThat(result1.data.person?.name).isEqualTo("Name111") + assertThat(result1.data.person?.age).isEqualTo(111) + assertThat(result2.data.person?.name).isEqualTo("Name222") + assertThat(result2.data.person?.age).isEqualTo(222) + assertThat(result3.data.person?.name).isEqualTo("Name333") + assertThat(result3.data.person?.age).isNull() } @Test @@ -84,12 +84,12 @@ class PersonSchemaTest { val result = schema.getPerson.execute(id = "IdOfPersonThatDoesNotExit") - assertThat(result.person).isNull() + assertThat(result.data.person).isNull() } @Test fun getAllPeopleShouldReturnEmptyListIfTheDatabaseIsEmpty(): Unit = runBlocking { - assertThat(schema.getAllPeople.execute().people).isEmpty() + assertThat(schema.getAllPeople.execute().data.people).isEmpty() } @Test @@ -100,7 +100,7 @@ class PersonSchemaTest { val result = schema.getAllPeople.execute() - assertThat(result.people) + assertThat(result.data.people) .containsExactly( PersonSchema.GetAllPeopleQuery.Data.Person(id = "111", name = "Name111", age = 111), PersonSchema.GetAllPeopleQuery.Data.Person(id = "222", name = "Name222", age = 222), diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt index 783455eb560..46e99dedcc2 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt @@ -25,5 +25,5 @@ internal constructor( internal val variablesSerializer: SerializationStrategy, internal val dataDeserializer: DeserializationStrategy, ) { - abstract suspend fun execute(variables: VariablesType): DataType + abstract suspend fun execute(variables: VariablesType): DataConnectResult } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 7b3517dbe2d..227077510ac 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -15,7 +15,11 @@ package com.google.firebase.dataconnect import android.content.Context import com.google.android.gms.security.ProviderInstaller +import com.google.protobuf.ListValue +import com.google.protobuf.Struct +import com.google.protobuf.Value import google.internal.firebase.firemat.v0.DataServiceGrpcKt.DataServiceCoroutineStub +import google.internal.firebase.firemat.v0.DataServiceOuterClass.GraphqlError import google.internal.firebase.firemat.v0.executeMutationRequest import google.internal.firebase.firemat.v0.executeQueryRequest import io.grpc.ManagedChannel @@ -85,7 +89,7 @@ internal class DataConnectGrpcClient( variables: VariablesType, variablesSerializer: SerializationStrategy, dataDeserializer: DeserializationStrategy - ): DataType { + ): DataConnectResult { val request = executeQueryRequest { this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName @@ -93,22 +97,14 @@ internal class DataConnectGrpcClient( } logger.debug { "executeQuery() sending request: $request" } - val response = - try { - grpcStub.executeQuery(request) - } catch (e: Throwable) { - logger.warn { "executeQuery() network transport error: $e" } - throw NetworkTransportException("query network transport error: ${e.message}", e) - } + val response = grpcStub.executeQuery(request) logger.debug { "executeQuery() got response: $response" } - if (response.errorsList.isNotEmpty()) { - throw GraphQLException( - "query failed: ${response.errorsList}", - response.errorsList.map { it.toString() } - ) - } - return decodeFromStruct(dataDeserializer, response.data) + return DataConnectResult( + variables = variables, + data = response.data.decode(dataDeserializer), + errors = response.errorsList.map { it.decode() } + ) } suspend fun executeMutation( @@ -118,7 +114,7 @@ internal class DataConnectGrpcClient( variables: VariablesType, variablesSerializer: SerializationStrategy, dataDeserializer: DeserializationStrategy - ): DataType { + ): DataConnectResult { val request = executeMutationRequest { this.name = name(operationSet = operationSet, revision = revision) this.operationName = operationName @@ -126,22 +122,14 @@ internal class DataConnectGrpcClient( } logger.debug { "executeMutation() sending request: $request" } - val response = - try { - grpcStub.executeMutation(request) - } catch (e: Throwable) { - logger.warn { "executeMutation() network transport error: $e" } - throw NetworkTransportException("mutation network transport error: ${e.message}", e) - } + val response = grpcStub.executeMutation(request) logger.debug { "executeMutation() got response: $response" } - if (response.errorsList.isNotEmpty()) { - throw GraphQLException( - "mutation failed: ${response.errorsList}", - response.errorsList.map { it.toString() } - ) - } - return decodeFromStruct(dataDeserializer, response.data) + return DataConnectResult( + variables = variables, + data = response.data.decode(dataDeserializer), + errors = response.errorsList.map { it.decode() } + ) } override fun toString(): String { @@ -160,3 +148,18 @@ internal class DataConnectGrpcClient( "projects/$projectId/locations/$location/services/$service/" + "operationSets/$operationSet/revisions/$revision" } + +fun Struct.decode(deserializer: DeserializationStrategy) = + decodeFromStruct(deserializer, this) + +fun ListValue.decodePath() = + valuesList.map { + when (val kind = it.kindCase) { + Value.KindCase.STRING_VALUE -> DataConnectError.PathSegment.Field(it.stringValue) + Value.KindCase.NUMBER_VALUE -> DataConnectError.PathSegment.ListIndex(it.numberValue.toInt()) + else -> throw IllegalStateException("invalid PathSegement kind: $kind") + } + } + +fun GraphqlError.decode() = + DataConnectError(message = message, path = path.decodePath(), extensions = emptyMap()) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt index 07976bf4bb2..096409bded8 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt @@ -18,13 +18,13 @@ private constructor(private val impl: Impl) { internal constructor( variables: VariablesType, - data: DataType?, + data: DataType, errors: List ) : this(Impl(variables = variables, data = data, errors = errors)) val variables: VariablesType get() = impl.variables - val data: DataType? + val data: DataType get() = impl.data val errors: List get() = impl.errors @@ -36,7 +36,7 @@ private constructor(private val impl: Impl) { private data class Impl( val variables: VariablesType, - val data: DataType?, + val data: DataType, val errors: List, ) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index c6ef0854cd5..1441b7da286 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -92,7 +92,10 @@ internal constructor( .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } } - internal suspend fun executeQuery(ref: QueryRef, variables: V): R = + internal suspend fun executeQuery( + ref: QueryRef, + variables: V + ): DataConnectResult = withContext(sequentialDispatcher) { grpcClient } .run { executeQuery( @@ -105,7 +108,10 @@ internal constructor( ) } - internal suspend fun executeMutation(ref: MutationRef, variables: V): R = + internal suspend fun executeMutation( + ref: MutationRef, + variables: V + ): DataConnectResult = withContext(sequentialDispatcher) { grpcClient } .run { executeMutation( @@ -117,6 +123,7 @@ internal constructor( dataDeserializer = ref.dataDeserializer ) } + override fun close() { logger.debug { "close() called" } runBlocking(sequentialDispatcher) { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt index 34d7ed38383..0922d4306f6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt @@ -33,6 +33,7 @@ internal constructor( variablesSerializer = variablesSerializer, dataDeserializer = dataDeserializer, ) { - override suspend fun execute(variables: VariablesType): DataType = - dataConnect.executeMutation(this, variables) + override suspend fun execute( + variables: VariablesType + ): DataConnectResult = dataConnect.executeMutation(this, variables) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt index c3d34a47b87..1c4b00590b7 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt @@ -33,8 +33,9 @@ internal constructor( variablesSerializer = variablesSerializer, dataDeserializer = dataDeserializer, ) { - override suspend fun execute(variables: VariablesType): DataType = - dataConnect.executeQuery(this, variables) + override suspend fun execute( + variables: VariablesType + ): DataConnectResult = dataConnect.executeQuery(this, variables) fun subscribe(variables: VariablesType): QuerySubscription = QuerySubscription(this, variables) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt index c653a03c2a1..7e18beb9f00 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt @@ -29,7 +29,7 @@ internal constructor( get() = _variables.get() private val sharedFlow = - MutableSharedFlow>( + MutableSharedFlow>( replay = 1, extraBufferCapacity = Integer.MAX_VALUE ) @@ -40,17 +40,18 @@ internal constructor( // NOTE: The variables below must ONLY be accessed from coroutines that use `sequentialDispatcher` // for their `CoroutineDispatcher`. Having this requirement removes the need for explicitly // synchronizing access to these variables. - private var inProgressReload: CompletableDeferred>? = null - private var pendingReload: CompletableDeferred>? = null + private var inProgressReload: CompletableDeferred>? = + null + private var pendingReload: CompletableDeferred>? = null - val lastResult: Message? + val lastResult: DataConnectResult? get() = sharedFlow.replayCache.firstOrNull() - fun reload(): Deferred> = + fun reload(): Deferred> = runBlocking(sequentialDispatcher) { pendingReload ?: run { - CompletableDeferred>().also { deferred -> + CompletableDeferred>().also { deferred -> if (inProgressReload == null) { inProgressReload = deferred query.dataConnect.coroutineScope.launch(sequentialDispatcher) { doReloadLoop() } @@ -66,33 +67,17 @@ internal constructor( reload() } - val flow: Flow> + val flow: Flow> get() = sharedFlow.asSharedFlow().onSubscription { reload() }.buffer(Channel.CONFLATED) private suspend fun doReloadLoop() { while (true) { val deferred = inProgressReload ?: break - val message = reload(variables, query) - deferred.complete(message) - sharedFlow.emit(message) + val result = query.execute(variables) + deferred.complete(result) + sharedFlow.emit(result) inProgressReload = pendingReload pendingReload = null } } - - class Message(val variables: VariablesType, val data: Result) } - -private suspend fun reload( - variables: VariablesType, - query: QueryRef -) = - QuerySubscription.Message( - variables = variables, - data = - try { - Result.success(query.execute(variables)) - } catch (e: Throwable) { - Result.failure(e) - } - ) From dec7de18bd29796f7b43e97b0a2511a86c01d896 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 22 Nov 2023 15:40:15 -0500 Subject: [PATCH 070/573] DataConnectResultTest.kt: fix build errors due to `data` changing from nullable to non-nullable --- .../dataconnect/DataConnectResultTest.kt | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt index d5161f2b163..7dc9fdcc31f 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt @@ -22,17 +22,6 @@ class DataConnectResultTest { assertThat(dataConnectResult.data).isSameInstanceAs(data) } - @Test - fun `data should be null if null was given to the constructor`() { - val dataConnectResult = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = null, - errors = SAMPLE_ERRORS - ) - assertThat(dataConnectResult.data).isNull() - } - @Test fun `errors should be the same object given to the constructor`() { val errors = listOf(SAMPLE_ERROR1) @@ -46,7 +35,7 @@ class DataConnectResultTest { val dataConnectResult = DataConnectResult( variables = SAMPLE_VARIABLES, - data = null, + data = SAMPLE_DATA, errors = emptyList() ) assertThat(dataConnectResult.errors).isEmpty() @@ -74,17 +63,6 @@ class DataConnectResultTest { assertThat(dataConnectResult.toString()).containsWithNonAdjacentText("TestDataToString") } - @Test - fun `toString() should incorporate the data, if null`() { - val dataConnectResult = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = null, - errors = emptyList() - ) - assertThat(dataConnectResult.toString()).containsWithNonAdjacentText("null") - } - @Test fun `toString() should incorporate the errors`() { val errors = listOf(SAMPLE_ERROR1, SAMPLE_ERROR2) From 08cdeff57f56ef1d8a4d5311892780d0d43958f7 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 22 Nov 2023 15:58:26 -0500 Subject: [PATCH 071/573] FirebaseDataConnectSettings.kt: replace builder with copy() --- .../firebase-dataconnect.gradle.kts | 2 + .../dataconnect/FirebaseDataConnectTest.kt | 145 +++++++++----- .../testutil/TestDataConnectFactory.kt | 8 +- .../dataconnect/FirebaseDataConnect.kt | 24 --- .../FirebaseDataConnectSettings.kt | 54 ++--- .../dataconnect/DataConnectErrorTest.kt | 1 + .../dataconnect/DataConnectResultTest.kt | 1 + .../FirebaseDataConnectSettingsTest.kt | 185 +++++++++--------- firebase-dataconnect/testutil/README.md | 4 + .../dataconnect/testutil}/TestUtils.kt | 2 +- .../testutil/testutil.gradle.kts | 40 ++++ 11 files changed, 255 insertions(+), 211 deletions(-) create mode 100644 firebase-dataconnect/testutil/README.md rename firebase-dataconnect/{src/test/kotlin/com/google/firebase/dataconnect => testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil}/TestUtils.kt (92%) create mode 100644 firebase-dataconnect/testutil/testutil.gradle.kts diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts index 2b16ba03d00..6c0ff6bb10c 100644 --- a/firebase-dataconnect/firebase-dataconnect.gradle.kts +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -103,10 +103,12 @@ dependencies { implementation(libs.protobuf.kotlin.lite) testCompileOnly(libs.protobuf.java) + testImplementation(project(":firebase-dataconnect:testutil")) testImplementation(libs.robolectric) testImplementation(libs.truth) testImplementation(libs.truth.liteproto.extension) + androidTestImplementation(project(":firebase-dataconnect:testutil")) androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.androidx.test.junit) androidTestImplementation(libs.androidx.test.rules) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index 2363f4d542b..fed8b3f80f8 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -23,6 +23,7 @@ import com.google.firebase.app import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory import com.google.firebase.dataconnect.testutil.TestFirebaseAppFactory +import com.google.firebase.dataconnect.testutil.containsWithNonAdjacentText import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.thread @@ -122,13 +123,19 @@ class FirebaseDataConnectTest { fun getInstance_should_return_the_cached_instance_if_settings_compare_equal() { val nonDefaultApp = firebaseAppFactory.newInstance() val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService") { - hostName = "TestHostName" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation", + "TestService", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName") + ) val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService") { - hostName = "TestHostName" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation", + "TestService", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName") + ) assertThat(instance1).isSameInstanceAs(instance2) } @@ -136,9 +143,12 @@ class FirebaseDataConnectTest { fun getInstance_should_return_the_cached_instance_if_settings_are_null() { val nonDefaultApp = firebaseAppFactory.newInstance() val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService") { - hostName = "TestHostName" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation", + "TestService", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName") + ) val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService", null) assertThat(instance1).isSameInstanceAs(instance2) @@ -148,20 +158,29 @@ class FirebaseDataConnectTest { fun getInstance_should_throw_if_settings_compare_unequal_to_settings_of_cached_instance() { val nonDefaultApp = firebaseAppFactory.newInstance() val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") { - hostName = "TestHostName1" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation1", + "TestService1", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") + ) assertThrows(IllegalArgumentException::class.java) { - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") { - hostName = "TestHostName2" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation1", + "TestService1", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2") + ) } val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") { - hostName = "TestHostName1" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation1", + "TestService1", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") + ) assertThat(instance1).isSameInstanceAs(instance2) } @@ -169,14 +188,20 @@ class FirebaseDataConnectTest { fun getInstance_should_allow_different_settings_after_first_instance_is_closed() { val nonDefaultApp = firebaseAppFactory.newInstance() val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService") { - hostName = "TestHostName1" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation", + "TestService", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") + ) instance1.close() val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService") { - hostName = "TestHostName2" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation", + "TestService", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2") + ) assertThat(instance1).isNotSameInstanceAs(instance2) } @@ -185,13 +210,19 @@ class FirebaseDataConnectTest { val nonDefaultApp1 = firebaseAppFactory.newInstance() val nonDefaultApp2 = firebaseAppFactory.newInstance() val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp1, "TestLocation", "TestService") { - hostName = "TestHostName1" - } + FirebaseDataConnect.getInstance( + nonDefaultApp1, + "TestLocation", + "TestService", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") + ) val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp2, "TestLocation", "TestService") { - hostName = "TestHostName2" - } + FirebaseDataConnect.getInstance( + nonDefaultApp2, + "TestLocation", + "TestService", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2") + ) assertThat(instance1).isNotSameInstanceAs(instance2) } @@ -199,28 +230,50 @@ class FirebaseDataConnectTest { fun getInstance_should_return_new_instance_if_settings_and_location_are_both_different() { val nonDefaultApp = firebaseAppFactory.newInstance() val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService") { - hostName = "TestHostName1" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation1", + "TestService", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") + ) val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService") { - hostName = "TestHostName2" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation2", + "TestService", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2") + ) + assertThat(instance1).isNotSameInstanceAs(instance2) + assertThat(instance1.settings) + .isEqualTo(FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1")) + assertThat(instance2.settings) + .isEqualTo(FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2")) } @Test fun getInstance_should_return_new_instance_if_settings_and_service_are_both_different() { val nonDefaultApp = firebaseAppFactory.newInstance() val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService1") { - hostName = "TestHostName1" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation", + "TestService1", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") + ) val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService2") { - hostName = "TestHostName2" - } + FirebaseDataConnect.getInstance( + nonDefaultApp, + "TestLocation", + "TestService2", + FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2") + ) + assertThat(instance1).isNotSameInstanceAs(instance2) + assertThat(instance1.settings) + .isEqualTo(FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1")) + assertThat(instance2.settings) + .isEqualTo(FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2")) } @Test @@ -289,9 +342,9 @@ class FirebaseDataConnectTest { val toStringResult = instance.toString() - assertThat(toStringResult).containsMatch("app=${app.name}\\W") - assertThat(toStringResult).containsMatch("projectId=${app.options.projectId}\\W") - assertThat(toStringResult).containsMatch("location=TestLocation\\W") - assertThat(toStringResult).containsMatch("service=TestService\\W") + assertThat(toStringResult).containsWithNonAdjacentText("app=${app.name}") + assertThat(toStringResult).containsWithNonAdjacentText("projectId=${app.options.projectId}") + assertThat(toStringResult).containsWithNonAdjacentText("location=TestLocation") + assertThat(toStringResult).containsWithNonAdjacentText("service=TestService") } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt index ac83ac8df5d..ae58257a523 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt @@ -15,6 +15,7 @@ package com.google.firebase.dataconnect.testutil import com.google.firebase.dataconnect.FirebaseDataConnect +import com.google.firebase.dataconnect.FirebaseDataConnectSettings /** * A JUnit test rule that creates instances of [FirebaseDataConnect] for use during testing, and @@ -29,10 +30,9 @@ class TestDataConnectFactory : override fun createInstance(instanceId: String, params: Params?) = FirebaseDataConnect.getInstance( location = params?.location ?: "TestLocation$instanceId", - service = params?.service ?: "TestService$instanceId" - ) { - connectToEmulator() - } + service = params?.service ?: "TestService$instanceId", + settings = FirebaseDataConnectSettings.emulator + ) override fun destroyInstance(instance: FirebaseDataConnect) { instance.close() diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 1441b7da286..075c00940b9 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -162,30 +162,6 @@ internal constructor( settings: FirebaseDataConnectSettings? = null ): FirebaseDataConnect = getInstance(app = Firebase.app, location = location, service = service, settings = settings) - - fun getInstance( - app: FirebaseApp, - location: String, - service: String, - settingsBlock: FirebaseDataConnectSettings.Builder.() -> Unit - ): FirebaseDataConnect = - getInstance( - app = app, - location = location, - service = service, - settings = FirebaseDataConnectSettings.defaults.build(settingsBlock) - ) - - fun getInstance( - location: String, - service: String, - settingsBlock: FirebaseDataConnectSettings.Builder.() -> Unit - ): FirebaseDataConnect = - getInstance( - location = location, - service = service, - settings = FirebaseDataConnectSettings.defaults.build(settingsBlock) - ) } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt index e4cf99d84b7..eb660d43b22 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt @@ -24,54 +24,28 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin val sslEnabled: Boolean get() = values.sslEnabled - val builder: Builder - get() = Builder(this) - - fun build(block: Builder.() -> Unit): FirebaseDataConnectSettings = builder.build(block) + fun copy( + hostName: String = this.hostName, + port: Int = this.port, + sslEnabled: Boolean = this.sslEnabled + ): FirebaseDataConnectSettings = + FirebaseDataConnectSettings( + SettingsValues(hostName = hostName, port = port, sslEnabled = sslEnabled) + ) companion object { - val defaults + val defaults: FirebaseDataConnectSettings get() = FirebaseDataConnectSettings( - SettingsValues( - hostName = "firestore.googleapis.com", - port = 443, - sslEnabled = true, - ) - ) - } - - class Builder internal constructor(initialValues: FirebaseDataConnectSettings) { - var hostName = initialValues.hostName - var port = initialValues.port - var sslEnabled = initialValues.sslEnabled - - fun connectToEmulator() { - hostName = "10.0.2.2" - port = 9510 - sslEnabled = false - } - - fun build(): FirebaseDataConnectSettings = - FirebaseDataConnectSettings( - SettingsValues( - hostName = hostName, - port = port, - sslEnabled = sslEnabled, + SettingsValues(hostName = "firestore.googleapis.com", port = 443, sslEnabled = true) ) - ) - fun build(block: Builder.() -> Unit): FirebaseDataConnectSettings = apply(block).build() + val emulator: FirebaseDataConnectSettings + get() = defaults.copy(hostName = "10.0.2.2", port = 9510, sslEnabled = false) } - override fun equals(other: Any?) = - if (other === this) { - true - } else if (other !is FirebaseDataConnectSettings) { - false - } else { - values == other.values - } + override fun equals(other: Any?): Boolean = + (other as? FirebaseDataConnectSettings)?.let { it.values == values } ?: false override fun hashCode() = values.hashCode() diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorTest.kt index eadf8145b86..e2bd92d3031 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorTest.kt @@ -2,6 +2,7 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat import com.google.firebase.dataconnect.DataConnectError.PathSegment +import com.google.firebase.dataconnect.testutil.containsWithNonAdjacentText import org.junit.Test class DataConnectErrorTest { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt index 7dc9fdcc31f..91c5563ddb7 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt @@ -2,6 +2,7 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat import com.google.firebase.dataconnect.DataConnectError.PathSegment +import com.google.firebase.dataconnect.testutil.containsWithNonAdjacentText import org.junit.Test class DataConnectResultTest { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt index a66240ceb8d..da6f1b12eb1 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt @@ -15,6 +15,8 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.testutil.containsWithNonAdjacentText +import java.util.UUID import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -23,7 +25,7 @@ import org.robolectric.RobolectricTestRunner class FirebaseDataConnectSettingsTest { @Test - fun `defaults properties should have the expected values`() { + fun `'defaults' companion property should have the expected values`() { FirebaseDataConnectSettings.defaults.apply { assertThat(hostName).isEqualTo("firestore.googleapis.com") assertThat(port).isEqualTo(443) @@ -32,61 +34,56 @@ class FirebaseDataConnectSettingsTest { } @Test - fun `builder should be initialized with values from the object given to the constructor`() { - val settings = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = 987654321 - sslEnabled = false - } - settings.builder.apply { - assertThat(hostName).isEqualTo("abc123") - assertThat(port).isEqualTo(987654321) + fun `'emulator' companion property should have the expected values`() { + FirebaseDataConnectSettings.emulator.apply { + assertThat(hostName).isEqualTo("10.0.2.2") + assertThat(port).isEqualTo(9510) assertThat(sslEnabled).isEqualTo(false) } } @Test - fun `builder can change the hostName`() { - val defaultSettings = FirebaseDataConnectSettings.defaults - val settings = defaultSettings.builder.build { hostName = "abc123" } - settings.apply { - assertThat(hostName).isEqualTo("abc123") - assertThat(port).isEqualTo(defaultSettings.port) - assertThat(sslEnabled).isEqualTo(defaultSettings.sslEnabled) - } + fun `copy() with no arguments should return a distinct, but equal, instance`() { + val settings = FirebaseDataConnectSettings.defaults + val settingsCopy = settings.copy() + assertThat(settings).isEqualTo(settingsCopy) + assertThat(settings).isNotSameInstanceAs(settingsCopy) } @Test - fun `builder can change the port`() { - val defaultSettings = FirebaseDataConnectSettings.defaults - val settings = defaultSettings.builder.build { port = 987654321 } - settings.apply { - assertThat(hostName).isEqualTo(defaultSettings.hostName) - assertThat(port).isEqualTo(987654321) - assertThat(sslEnabled).isEqualTo(defaultSettings.sslEnabled) - } + fun `copy() can change the hostName`() { + val originalSettings = FirebaseDataConnectSettings.defaults + val newHostName = UUID.randomUUID().toString() + + val modifiedSettings = originalSettings.copy(hostName = newHostName) + + assertThat(modifiedSettings.hostName).isEqualTo(newHostName) + assertThat(modifiedSettings.port).isEqualTo(originalSettings.port) + assertThat(modifiedSettings.sslEnabled).isEqualTo(originalSettings.sslEnabled) } @Test - fun `builder can change the sslEnabled`() { - val defaultSettings = FirebaseDataConnectSettings.defaults - val settings = defaultSettings.builder.build { sslEnabled = !defaultSettings.sslEnabled } - settings.apply { - assertThat(hostName).isEqualTo(defaultSettings.hostName) - assertThat(port).isEqualTo(defaultSettings.port) - assertThat(sslEnabled).isEqualTo(!defaultSettings.sslEnabled) - } + fun `copy() can change the port`() { + val originalSettings = FirebaseDataConnectSettings.defaults + val newPort = originalSettings.port + 42 + + val modifiedSettings = originalSettings.copy(port = newPort) + + assertThat(modifiedSettings.hostName).isEqualTo(originalSettings.hostName) + assertThat(modifiedSettings.port).isEqualTo(newPort) + assertThat(modifiedSettings.sslEnabled).isEqualTo(originalSettings.sslEnabled) } @Test - fun `connectToEmulator() should set the correct values`() { - FirebaseDataConnectSettings.defaults.builder.apply { - connectToEmulator() - assertThat(hostName).isEqualTo("10.0.2.2") - assertThat(port).isEqualTo(9510) - assertThat(sslEnabled).isEqualTo(false) - } + fun `copy() can change the sslEnabled`() { + val originalSettings = FirebaseDataConnectSettings.defaults + val newSslEnabled = !originalSettings.sslEnabled + + val modifiedSettings = originalSettings.copy(sslEnabled = newSslEnabled) + + assertThat(modifiedSettings.hostName).isEqualTo(originalSettings.hostName) + assertThat(modifiedSettings.port).isEqualTo(originalSettings.port) + assertThat(modifiedSettings.sslEnabled).isEqualTo(newSslEnabled) } @Test @@ -112,17 +109,17 @@ class FirebaseDataConnectSettingsTest { @Suppress("ReplaceCallWithBinaryOperator") fun `equals() should return true for distinct instances with the same property values`() { val settings1 = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = 987654321 + FirebaseDataConnectSettings.defaults.copy( + hostName = "abc123", + port = 987654321, sslEnabled = true - } + ) val settings2 = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = 987654321 + FirebaseDataConnectSettings.defaults.copy( + hostName = "abc123", + port = 987654321, sslEnabled = true - } + ) // validate the assumption that `settings1` and `settings2` are distinct objects. assertThat(settings1).isNotSameInstanceAs(settings2) assertThat(settings1.equals(settings2)).isTrue() @@ -132,17 +129,17 @@ class FirebaseDataConnectSettingsTest { @Suppress("ReplaceCallWithBinaryOperator") fun `equals() should return false when hostName differs`() { val settings1 = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = 987654321 + FirebaseDataConnectSettings.defaults.copy( + hostName = "abc123", + port = 987654321, sslEnabled = true - } + ) val settings2 = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "zzzzzz" - port = 987654321 + FirebaseDataConnectSettings.defaults.copy( + hostName = "zzzzzz", + port = 987654321, sslEnabled = true - } + ) assertThat(settings1.equals(settings2)).isFalse() } @@ -150,17 +147,13 @@ class FirebaseDataConnectSettingsTest { @Suppress("ReplaceCallWithBinaryOperator") fun `equals() should return false when port differs`() { val settings1 = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = 987654321 + FirebaseDataConnectSettings.defaults.copy( + hostName = "abc123", + port = 987654321, sslEnabled = true - } + ) val settings2 = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = -1 - sslEnabled = true - } + FirebaseDataConnectSettings.defaults.copy(hostName = "abc123", port = -1, sslEnabled = true) assertThat(settings1.equals(settings2)).isFalse() } @@ -168,17 +161,17 @@ class FirebaseDataConnectSettingsTest { @Suppress("ReplaceCallWithBinaryOperator") fun `equals() should return false when sslEnabled differs`() { val settings1 = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = 987654321 + FirebaseDataConnectSettings.defaults.copy( + hostName = "abc123", + port = 987654321, sslEnabled = true - } + ) val settings2 = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = 987654321 + FirebaseDataConnectSettings.defaults.copy( + hostName = "abc123", + port = 987654321, sslEnabled = false - } + ) assertThat(settings1.equals(settings2)).isFalse() } @@ -191,17 +184,17 @@ class FirebaseDataConnectSettingsTest { @Test fun `hashCode() should return the same value when invoked on a distinct, but equal object`() { val settings1 = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = 987654321 + FirebaseDataConnectSettings.defaults.copy( + hostName = "abc123", + port = 987654321, sslEnabled = true - } + ) val settings2 = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = 987654321 + FirebaseDataConnectSettings.defaults.copy( + hostName = "abc123", + port = 987654321, sslEnabled = true - } + ) // validate the assumption that `settings1` and `settings2` are distinct objects. assertThat(settings1).isNotSameInstanceAs(settings2) assertThat(settings1.hashCode()).isEqualTo(settings2.hashCode()) @@ -209,36 +202,36 @@ class FirebaseDataConnectSettingsTest { @Test fun `hashCode() should return the different values when hostName differs`() { - val settings1 = FirebaseDataConnectSettings.defaults.builder.build { hostName = "abc123" } - val settings2 = FirebaseDataConnectSettings.defaults.builder.build { hostName = "xyz987" } + val settings1 = FirebaseDataConnectSettings.defaults.copy(hostName = "abc123") + val settings2 = FirebaseDataConnectSettings.defaults.copy(hostName = "xyz987") assertThat(settings1.hashCode()).isNotEqualTo(settings2.hashCode()) } @Test fun `hashCode() should return the different values when port differs`() { - val settings1 = FirebaseDataConnectSettings.defaults.builder.build { port = 987 } - val settings2 = FirebaseDataConnectSettings.defaults.builder.build { port = 123 } + val settings1 = FirebaseDataConnectSettings.defaults.copy(port = 987) + val settings2 = FirebaseDataConnectSettings.defaults.copy(port = 123) assertThat(settings1.hashCode()).isNotEqualTo(settings2.hashCode()) } @Test fun `hashCode() should return the different values when sslEnabled differs`() { - val settings1 = FirebaseDataConnectSettings.defaults.builder.build { sslEnabled = true } - val settings2 = FirebaseDataConnectSettings.defaults.builder.build { sslEnabled = false } + val settings1 = FirebaseDataConnectSettings.defaults.copy(sslEnabled = true) + val settings2 = FirebaseDataConnectSettings.defaults.copy(sslEnabled = false) assertThat(settings1.hashCode()).isNotEqualTo(settings2.hashCode()) } @Test fun `toString() should return a string that contains the property values`() { val settings = - FirebaseDataConnectSettings.defaults.builder.build { - hostName = "abc123" - port = 987654321 + FirebaseDataConnectSettings.defaults.copy( + hostName = "abc123", + port = 987654321, sslEnabled = true - } + ) val toStringResult = settings.toString() - assertThat(toStringResult).containsMatch("hostName=abc123\\W") - assertThat(toStringResult).containsMatch("port=987654321\\W") - assertThat(toStringResult).containsMatch("sslEnabled=true\\W") + assertThat(toStringResult).containsWithNonAdjacentText("hostName=abc123") + assertThat(toStringResult).containsWithNonAdjacentText("port=987654321") + assertThat(toStringResult).containsWithNonAdjacentText("sslEnabled=true") } } diff --git a/firebase-dataconnect/testutil/README.md b/firebase-dataconnect/testutil/README.md new file mode 100644 index 00000000000..04ac7a031bd --- /dev/null +++ b/firebase-dataconnect/testutil/README.md @@ -0,0 +1,4 @@ +An android library project to share code between the firebase-dataconnect +unit tests _and_ instrumentation tests. + +See https://github.com/android/architecture-samples and https://stackoverflow.com/q/72218645 diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/TestUtils.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt similarity index 92% rename from firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/TestUtils.kt rename to firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt index c1c2080c04b..c899eb38b73 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/TestUtils.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt @@ -1,4 +1,4 @@ -package com.google.firebase.dataconnect +package com.google.firebase.dataconnect.testutil import com.google.common.truth.StringSubject import java.util.regex.Pattern diff --git a/firebase-dataconnect/testutil/testutil.gradle.kts b/firebase-dataconnect/testutil/testutil.gradle.kts new file mode 100644 index 00000000000..9eefdb1673b --- /dev/null +++ b/firebase-dataconnect/testutil/testutil.gradle.kts @@ -0,0 +1,40 @@ +// Copyright 2023 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. + +plugins { + id("android-library") + id("kotlin-android") +} + +android { + val targetSdkVersion: Int by rootProject + + namespace = "com.google.firebase.dataconnect.testutil" + compileSdk = 33 + defaultConfig { + minSdk = 21 + targetSdk = targetSdkVersion + multiDexEnabled = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { jvmTarget = "1.8" } +} + +dependencies { + api(libs.truth) +} From 413eff9857e86691697e47567779c0e89ea79cc7 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 23 Nov 2023 11:37:36 -0500 Subject: [PATCH 072/573] FirebaseDataConnect.ServiceConfig added (#515) --- .../dataconnect/FirebaseDataConnectTest.kt | 302 ++++++++++++---- .../dataconnect/QuerySubscriptionTest.kt | 9 +- .../dataconnect/generated/PostsTest.kt | 43 ++- .../testutil/EmulatorController.kt | 2 +- .../dataconnect/testutil/FactoryTestRule.kt | 17 +- .../testutil/TestDataConnectFactory.kt | 41 ++- .../testutil/TestFirebaseAppFactory.kt | 5 +- .../testutil/schemas/PersonSchema.kt | 140 ++++---- .../testutil/schemas/PersonSchemaTest.kt | 13 +- .../google/firebase/dataconnect/BaseRef.kt | 2 - .../dataconnect/DataConnectGrpcClient.kt | 38 +- .../dataconnect/FirebaseDataConnect.kt | 115 +++--- .../dataconnect/FirebaseDataConnectFactory.kt | 22 +- .../firebase/dataconnect/MutationRef.kt | 4 - .../google/firebase/dataconnect/QueryRef.kt | 4 - .../generated/CreatePostMutation.kt | 16 +- .../dataconnect/generated/GetPostQuery.kt | 19 +- .../firebase/dataconnect/generated/Posts.kt | 59 ++++ .../FirebaseDataConnectServiceConfigTest.kt | 327 ++++++++++++++++++ .../FirebaseDataConnectSettingsTest.kt | 4 +- .../dataconnect/testutil/TestUtils.kt | 29 ++ firebase-messaging/CHANGELOG.md | 2 + .../messaging/FirebaseMessagingService.java | 18 + .../FirebaseMessagingServiceRoboTest.java | 45 ++- subprojects.cfg | 1 + 25 files changed, 947 insertions(+), 330 deletions(-) create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectServiceConfigTest.kt diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt index fed8b3f80f8..6582ce97dcb 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt @@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app +import com.google.firebase.dataconnect.FirebaseDataConnect.ServiceConfig import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory import com.google.firebase.dataconnect.testutil.TestFirebaseAppFactory @@ -42,13 +43,14 @@ class FirebaseDataConnectTest { @Test fun getInstance_without_specifying_an_app_should_use_the_default_app() { - val instance1 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation1", "TestService1") - val instance2 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation2", "TestService2") + val instance1 = FirebaseDataConnect.getInstance(Firebase.app, SAMPLE_SERVICE_CONFIG1) + val instance2 = FirebaseDataConnect.getInstance(Firebase.app, SAMPLE_SERVICE_CONFIG2) + // Validate the assumption that different location and service yield distinct instances. assertThat(instance1).isNotSameInstanceAs(instance2) - val instance1DefaultApp = FirebaseDataConnect.getInstance("TestLocation1", "TestService1") - val instance2DefaultApp = FirebaseDataConnect.getInstance("TestLocation2", "TestService2") + val instance1DefaultApp = FirebaseDataConnect.getInstance(SAMPLE_SERVICE_CONFIG1) + val instance2DefaultApp = FirebaseDataConnect.getInstance(SAMPLE_SERVICE_CONFIG2) assertThat(instance1DefaultApp).isSameInstanceAs(instance1) assertThat(instance2DefaultApp).isSameInstanceAs(instance2) @@ -56,22 +58,22 @@ class FirebaseDataConnectTest { @Test fun getInstance_with_default_app_should_return_non_null() { - val instance = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") + val instance = FirebaseDataConnect.getInstance(Firebase.app, SAMPLE_SERVICE_CONFIG1) assertThat(instance).isNotNull() } @Test fun getInstance_with_default_app_should_return_the_same_instance_every_time() { - val instance1 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") - val instance2 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") + val instance1 = FirebaseDataConnect.getInstance(Firebase.app, SAMPLE_SERVICE_CONFIG1) + val instance2 = FirebaseDataConnect.getInstance(Firebase.app, SAMPLE_SERVICE_CONFIG1) assertThat(instance1).isSameInstanceAs(instance2) } @Test fun getInstance_should_return_new_instance_after_terminate() { - val instance1 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") + val instance1 = FirebaseDataConnect.getInstance(Firebase.app, SAMPLE_SERVICE_CONFIG1) instance1.close() - val instance2 = FirebaseDataConnect.getInstance(Firebase.app, "TestLocation", "TestService") + val instance2 = FirebaseDataConnect.getInstance(Firebase.app, SAMPLE_SERVICE_CONFIG1) assertThat(instance1).isNotSameInstanceAs(instance2) } @@ -79,41 +81,70 @@ class FirebaseDataConnectTest { fun getInstance_should_return_distinct_instances_for_distinct_apps() { val nonDefaultApp1 = firebaseAppFactory.newInstance() val nonDefaultApp2 = firebaseAppFactory.newInstance() - val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp1, "TestLocation", "TestService") - val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp2, "TestLocation", "TestService") + val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp1, SAMPLE_SERVICE_CONFIG1) + val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp2, SAMPLE_SERVICE_CONFIG1) + assertThat(instance1).isNotSameInstanceAs(instance2) + } + + @Test + fun getInstance_should_return_distinct_instances_for_distinct_serviceIds() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val serviceConfig1 = SAMPLE_SERVICE_CONFIG1.withServiceId("foo") + val serviceConfig2 = serviceConfig1.withServiceId("bar") + val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig1) + val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig2) assertThat(instance1).isNotSameInstanceAs(instance2) } @Test fun getInstance_should_return_distinct_instances_for_distinct_locations() { val nonDefaultApp = firebaseAppFactory.newInstance() - val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService") - val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService") + val serviceConfig1 = SAMPLE_SERVICE_CONFIG1.withLocation("foo") + val serviceConfig2 = serviceConfig1.withLocation("bar") + val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig1) + val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig2) assertThat(instance1).isNotSameInstanceAs(instance2) } @Test - fun getInstance_should_return_distinct_instances_for_distinct_services() { + fun getInstance_should_return_distinct_instances_for_distinct_operationSets() { val nonDefaultApp = firebaseAppFactory.newInstance() - val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService1") - val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService2") + val serviceConfig1 = SAMPLE_SERVICE_CONFIG1.withOperationSet("foo") + val serviceConfig2 = serviceConfig1.withOperationSet("bar") + val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig1) + val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig2) assertThat(instance1).isNotSameInstanceAs(instance2) } + @Test + fun getInstance_should_throw_if_revision_differs_from_that_of_cached_instance() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val serviceConfig1 = SAMPLE_SERVICE_CONFIG1.withRevision("foo") + val serviceConfig2 = serviceConfig1.withRevision("bar") + val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig1) + + assertThrows(IllegalArgumentException::class.java) { + FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig2) + } + + val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig1) + assertThat(instance1).isSameInstanceAs(instance2) + } + @Test fun getInstance_should_return_a_new_instance_after_the_instance_is_terminated() { val nonDefaultApp = firebaseAppFactory.newInstance() - val instance1A = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") - val instance2A = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService2") + val instance1A = FirebaseDataConnect.getInstance(nonDefaultApp, SAMPLE_SERVICE_CONFIG1) + val instance2A = FirebaseDataConnect.getInstance(nonDefaultApp, SAMPLE_SERVICE_CONFIG2) assertThat(instance1A).isNotSameInstanceAs(instance2A) instance1A.close() - val instance1B = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation1", "TestService1") + val instance1B = FirebaseDataConnect.getInstance(nonDefaultApp, SAMPLE_SERVICE_CONFIG1) assertThat(instance1A).isNotSameInstanceAs(instance1B) assertThat(instance1A).isNotSameInstanceAs(instance2A) instance2A.close() - val instance2B = FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation2", "TestService2") + val instance2B = FirebaseDataConnect.getInstance(nonDefaultApp, SAMPLE_SERVICE_CONFIG2) assertThat(instance2A).isNotSameInstanceAs(instance2B) assertThat(instance2A).isNotSameInstanceAs(instance1A) assertThat(instance2A).isNotSameInstanceAs(instance1B) @@ -125,15 +156,13 @@ class FirebaseDataConnectTest { val instance1 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation", - "TestService", + SAMPLE_SERVICE_CONFIG1, FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName") ) val instance2 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation", - "TestService", + SAMPLE_SERVICE_CONFIG1, FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName") ) assertThat(instance1).isSameInstanceAs(instance2) @@ -145,12 +174,10 @@ class FirebaseDataConnectTest { val instance1 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation", - "TestService", + SAMPLE_SERVICE_CONFIG1, FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName") ) - val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp, "TestLocation", "TestService", null) + val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, SAMPLE_SERVICE_CONFIG1, null) assertThat(instance1).isSameInstanceAs(instance2) } @@ -160,16 +187,14 @@ class FirebaseDataConnectTest { val instance1 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation1", - "TestService1", + SAMPLE_SERVICE_CONFIG1, FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") ) assertThrows(IllegalArgumentException::class.java) { FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation1", - "TestService1", + SAMPLE_SERVICE_CONFIG1, FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2") ) } @@ -177,8 +202,7 @@ class FirebaseDataConnectTest { val instance2 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation1", - "TestService1", + SAMPLE_SERVICE_CONFIG1, FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") ) assertThat(instance1).isSameInstanceAs(instance2) @@ -190,21 +214,30 @@ class FirebaseDataConnectTest { val instance1 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation", - "TestService", + SAMPLE_SERVICE_CONFIG1, FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") ) instance1.close() val instance2 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation", - "TestService", + SAMPLE_SERVICE_CONFIG1, FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2") ) assertThat(instance1).isNotSameInstanceAs(instance2) } + @Test + fun getInstance_should_allow_different_revision_after_first_instance_is_closed() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val instance1 = + FirebaseDataConnect.getInstance(nonDefaultApp, SAMPLE_SERVICE_CONFIG1.withRevision("foo")) + instance1.close() + val instance2 = + FirebaseDataConnect.getInstance(nonDefaultApp, SAMPLE_SERVICE_CONFIG1.withRevision("bar")) + assertThat(instance1).isNotSameInstanceAs(instance2) + } + @Test fun getInstance_should_return_new_instance_if_settings_and_app_are_both_different() { val nonDefaultApp1 = firebaseAppFactory.newInstance() @@ -212,35 +245,42 @@ class FirebaseDataConnectTest { val instance1 = FirebaseDataConnect.getInstance( nonDefaultApp1, - "TestLocation", - "TestService", + SAMPLE_SERVICE_CONFIG1, FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") ) val instance2 = FirebaseDataConnect.getInstance( nonDefaultApp2, - "TestLocation", - "TestService", + SAMPLE_SERVICE_CONFIG1, FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2") ) assertThat(instance1).isNotSameInstanceAs(instance2) } @Test - fun getInstance_should_return_new_instance_if_settings_and_location_are_both_different() { + fun getInstance_should_return_new_instance_if_revision_and_app_are_both_different() { + val nonDefaultApp1 = firebaseAppFactory.newInstance() + val nonDefaultApp2 = firebaseAppFactory.newInstance() + val instance1 = + FirebaseDataConnect.getInstance(nonDefaultApp1, SAMPLE_SERVICE_CONFIG1.withRevision("foo")) + val instance2 = + FirebaseDataConnect.getInstance(nonDefaultApp2, SAMPLE_SERVICE_CONFIG1.withRevision("bar")) + assertThat(instance1).isNotSameInstanceAs(instance2) + } + + @Test + fun getInstance_should_return_new_instance_if_settings_and_serviceId_are_both_different() { val nonDefaultApp = firebaseAppFactory.newInstance() val instance1 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation1", - "TestService", + SAMPLE_SERVICE_CONFIG1.withServiceId("foo"), FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") ) val instance2 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation2", - "TestService", + SAMPLE_SERVICE_CONFIG1.withServiceId("bar"), FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2") ) @@ -252,20 +292,37 @@ class FirebaseDataConnectTest { } @Test - fun getInstance_should_return_new_instance_if_settings_and_service_are_both_different() { + fun getInstance_should_return_new_instance_if_revision_and_serviceId_are_both_different() { val nonDefaultApp = firebaseAppFactory.newInstance() val instance1 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation", - "TestService1", + SAMPLE_SERVICE_CONFIG1.withServiceId("foo").withRevision("boo") + ) + val instance2 = + FirebaseDataConnect.getInstance( + nonDefaultApp, + SAMPLE_SERVICE_CONFIG1.withServiceId("bar").withRevision("zoo") + ) + + assertThat(instance1).isNotSameInstanceAs(instance2) + assertThat(instance1.serviceConfig.revision).isEqualTo("boo") + assertThat(instance2.serviceConfig.revision).isEqualTo("zoo") + } + + @Test + fun getInstance_should_return_new_instance_if_settings_and_location_are_both_different() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val instance1 = + FirebaseDataConnect.getInstance( + nonDefaultApp, + SAMPLE_SERVICE_CONFIG1.withLocation("foo"), FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName1") ) val instance2 = FirebaseDataConnect.getInstance( nonDefaultApp, - "TestLocation", - "TestService2", + SAMPLE_SERVICE_CONFIG1.withLocation("bar"), FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2") ) @@ -276,6 +333,25 @@ class FirebaseDataConnectTest { .isEqualTo(FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2")) } + @Test + fun getInstance_should_return_new_instance_if_revision_and_location_are_both_different() { + val nonDefaultApp = firebaseAppFactory.newInstance() + val instance1 = + FirebaseDataConnect.getInstance( + nonDefaultApp, + SAMPLE_SERVICE_CONFIG1.withLocation("foo").withRevision("boo") + ) + val instance2 = + FirebaseDataConnect.getInstance( + nonDefaultApp, + SAMPLE_SERVICE_CONFIG1.withLocation("bar").withRevision("zoo") + ) + + assertThat(instance1).isNotSameInstanceAs(instance2) + assertThat(instance1.serviceConfig.revision).isEqualTo("boo") + assertThat(instance2.serviceConfig.revision).isEqualTo("zoo") + } + @Test fun getInstance_should_be_thread_safe() { val apps = @@ -290,31 +366,27 @@ class FirebaseDataConnectTest { val createdInstancesByThreadId = mutableMapOf>() val numThreads = 8 - val threads = - mutableListOf().run { - val readyCountDown = AtomicInteger(numThreads) - repeat(numThreads) { i -> - add( - thread { - readyCountDown.decrementAndGet() - while (readyCountDown.get() > 0) { - /* spin */ + val threads = buildList { + val readyCountDown = AtomicInteger(numThreads) + repeat(numThreads) { i -> + add( + thread { + readyCountDown.decrementAndGet() + while (readyCountDown.get() > 0) { + /* spin */ + } + val instances = buildList { + for (app in apps) { + add(FirebaseDataConnect.getInstance(app, SAMPLE_SERVICE_CONFIG1)) + add(FirebaseDataConnect.getInstance(app, SAMPLE_SERVICE_CONFIG2)) + add(FirebaseDataConnect.getInstance(app, SAMPLE_SERVICE_CONFIG3)) } - val instances = - mutableListOf().run { - for (app in apps) { - add(FirebaseDataConnect.getInstance(app, "TestLocation1", "TestService1")) - add(FirebaseDataConnect.getInstance(app, "TestLocation2", "TestService2")) - add(FirebaseDataConnect.getInstance(app, "TestLocation3", "TestService3")) - } - toList() - } - createdInstancesByThreadIdLock.withLock { createdInstancesByThreadId[i] = instances } } - ) - } - toList() + createdInstancesByThreadIdLock.withLock { createdInstancesByThreadId[i] = instances } + } + ) } + } threads.forEach { it.join() } @@ -338,13 +410,91 @@ class FirebaseDataConnectTest { fun toString_should_return_a_string_that_contains_the_required_information() { val app = firebaseAppFactory.newInstance() val instance = - FirebaseDataConnect.getInstance(app = app, location = "TestLocation", service = "TestService") + FirebaseDataConnect.getInstance( + app = app, + ServiceConfig( + serviceId = "TestServiceId", + location = "TestLocation", + operationSet = "TestOperationSet", + revision = "TestRevision" + ) + ) val toStringResult = instance.toString() assertThat(toStringResult).containsWithNonAdjacentText("app=${app.name}") assertThat(toStringResult).containsWithNonAdjacentText("projectId=${app.options.projectId}") + assertThat(toStringResult).containsWithNonAdjacentText("serviceId=TestServiceId") assertThat(toStringResult).containsWithNonAdjacentText("location=TestLocation") - assertThat(toStringResult).containsWithNonAdjacentText("service=TestService") + assertThat(toStringResult).containsWithNonAdjacentText("operationSet=TestOperationSet") + assertThat(toStringResult).containsWithNonAdjacentText("revision=TestRevision") } } + +private val SAMPLE_SERVICE_ID1 = "SampleServiceId1" +private val SAMPLE_LOCATION1 = "SampleLocation1" +private val SAMPLE_OPERATION_SET1 = "SampleOperationSet1" +private val SAMPLE_REVISION1 = "SampleRevision1" +private val SAMPLE_SERVICE_CONFIG1 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID1, + location = SAMPLE_LOCATION1, + operationSet = SAMPLE_OPERATION_SET1, + revision = SAMPLE_REVISION1 + ) + +private val SAMPLE_SERVICE_ID2 = "SampleServiceId2" +private val SAMPLE_LOCATION2 = "SampleLocation2" +private val SAMPLE_OPERATION_SET2 = "SampleOperationSet2" +private val SAMPLE_REVISION2 = "SampleRevision2" +private val SAMPLE_SERVICE_CONFIG2 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID2, + location = SAMPLE_LOCATION2, + operationSet = SAMPLE_OPERATION_SET2, + revision = SAMPLE_REVISION2 + ) + +private val SAMPLE_SERVICE_ID3 = "SampleServiceId3" +private val SAMPLE_LOCATION3 = "SampleLocation3" +private val SAMPLE_OPERATION_SET3 = "SampleOperationSet3" +private val SAMPLE_REVISION3 = "SampleRevision3" +private val SAMPLE_SERVICE_CONFIG3 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID3, + location = SAMPLE_LOCATION3, + operationSet = SAMPLE_OPERATION_SET3, + revision = SAMPLE_REVISION3 + ) + +private fun ServiceConfig.withServiceId(newServiceId: String) = + ServiceConfig( + serviceId = newServiceId, + location = location, + operationSet = operationSet, + revision = revision, + ) + +private fun ServiceConfig.withLocation(newLocation: String) = + ServiceConfig( + serviceId = serviceId, + location = newLocation, + operationSet = operationSet, + revision = revision, + ) + +private fun ServiceConfig.withOperationSet(newOperationSet: String) = + ServiceConfig( + serviceId = serviceId, + location = location, + operationSet = newOperationSet, + revision = revision, + ) + +private fun ServiceConfig.withRevision(newRevision: String) = + ServiceConfig( + serviceId = serviceId, + location = location, + operationSet = operationSet, + revision = newRevision, + ) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt index 7e518aa6384..d33f4f47cb3 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt @@ -22,10 +22,11 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory -import com.google.firebase.dataconnect.testutil.schemas.CreatePersonMutationExt.execute -import com.google.firebase.dataconnect.testutil.schemas.GetPersonQueryExt.subscribe import com.google.firebase.dataconnect.testutil.schemas.PersonSchema -import com.google.firebase.dataconnect.testutil.schemas.UpdatePersonMutationExt.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.Companion.newPersonSchema +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.subscribe +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute import java.util.concurrent.Executors import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -47,7 +48,7 @@ class QuerySubscriptionTest { @Before fun initializePersonSchema() { - schema = PersonSchema(dataConnectFactory.newInstance()) + schema = dataConnectFactory.newPersonSchema() runBlocking { schema.installEmulatorSchema() } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt index f9d0992b1cd..ca716cb8d8e 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt @@ -16,9 +16,12 @@ package com.google.firebase.dataconnect.generated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertWithMessage +import com.google.firebase.Firebase +import com.google.firebase.app +import com.google.firebase.dataconnect.FirebaseDataConnectSettings import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory -import java.util.UUID +import com.google.firebase.dataconnect.testutil.nextAlphanumericString import kotlin.math.absoluteValue import kotlin.random.Random import kotlin.time.Duration.Companion.seconds @@ -29,6 +32,7 @@ import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -40,26 +44,37 @@ class PostsTest { @get:Rule val dataConnectFactory = TestDataConnectFactory() @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + private lateinit var posts: PostsOperationSet + + @Before + fun setUpPostsOperationSet() { + posts = + PostsOperationSet( + app = Firebase.app, + serviceId = "local", + location = Random.nextAlphanumericString(), + settings = FirebaseDataConnectSettings.emulator + ) + dataConnectFactory.adoptInstance(posts.dataConnect) + } + @Test fun getPostWithNonExistingId() { - val dc = dataConnectFactory.newInstance(service = "local") runBlocking { - val queryResponse = dc.queries.getPost.execute(id = UUID.randomUUID().toString()) + val queryResponse = posts.getPost.execute(id = Random.nextAlphanumericString()) assertWithMessage("queryResponse").that(queryResponse.data.post).isNull() } } @Test fun createPostThenGetPost() { - val dc = dataConnectFactory.newInstance(service = "local") - - val postId = UUID.randomUUID().toString() + val postId = Random.nextAlphanumericString() val postContent = Random.Default.nextLong().toString(30) runBlocking { - dc.mutations.createPost.execute(id = postId, content = postContent) + posts.createPost.execute(id = postId, content = postContent) - val queryResponse = dc.queries.getPost.execute(id = postId) + val queryResponse = posts.getPost.execute(id = postId) assertWithMessage("queryResponse") .that(queryResponse.data.post) .isEqualTo(GetPostQuery.Data.Post(content = postContent, comments = emptyList())) @@ -68,18 +83,16 @@ class PostsTest { @Test fun subscribe() { - val dc = dataConnectFactory.newInstance(service = "local") - - val postId1 = UUID.randomUUID().toString() + val postId1 = Random.nextAlphanumericString() val postContent1 = Random.Default.nextLong().absoluteValue.toString(30) - val postId2 = UUID.randomUUID().toString() + val postId2 = Random.nextAlphanumericString() val postContent2 = Random.Default.nextLong().absoluteValue.toString(30) runBlocking { - dc.mutations.createPost.execute(id = postId1, content = postContent1) - dc.mutations.createPost.execute(id = postId2, content = postContent2) + posts.createPost.execute(id = postId1, content = postContent1) + posts.createPost.execute(id = postId2, content = postContent2) - val querySubscription = dc.queries.getPost.subscribe(id = postId1) + val querySubscription = posts.getPost.subscribe(id = postId1) assertWithMessage("lastResult 0").that(querySubscription.lastResult).isNull() val result1 = querySubscription.flow.timeout(5.seconds).first() diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt index e8d6f44a192..f788efced06 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt @@ -35,7 +35,7 @@ suspend fun FirebaseDataConnect.installEmulatorSchema( try { setupSchema( EmulatorServiceCoroutineStub(grpcChannel), - serviceId = service, + serviceId = serviceConfig.serviceId, schema = schema, operationSets = operationSets ) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt index d12a345d27b..99c657048eb 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/FactoryTestRule.kt @@ -1,6 +1,5 @@ package com.google.firebase.dataconnect.testutil -import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import org.junit.rules.ExternalResource @@ -18,11 +17,18 @@ abstract class FactoryTestRule : ExternalResource() { if (!active.get()) { throw IllegalStateException("newInstance() may only be called during the test's execution") } - val instance = createInstance(generateRandomUid(), params) + val instance = createInstance(params) instances.add(instance) return instance } + fun adoptInstance(instance: T) { + if (!active.get()) { + throw IllegalStateException("adoptInstance() may only be called during the test's execution") + } + instances.add(instance) + } + override fun before() { active.set(true) } @@ -34,11 +40,6 @@ abstract class FactoryTestRule : ExternalResource() { } } - private fun generateRandomUid(): String = - UUID.randomUUID().let { - it.leastSignificantBits.toString(30) + it.mostSignificantBits.toString(30) - } - - protected abstract fun createInstance(instanceId: String, params: P?): T + protected abstract fun createInstance(params: P?): T protected abstract fun destroyInstance(instance: T) } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt index ae58257a523..94c7dd49921 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt @@ -16,6 +16,7 @@ package com.google.firebase.dataconnect.testutil import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.FirebaseDataConnectSettings +import kotlin.random.Random /** * A JUnit test rule that creates instances of [FirebaseDataConnect] for use during testing, and @@ -24,19 +25,41 @@ import com.google.firebase.dataconnect.FirebaseDataConnectSettings class TestDataConnectFactory : FactoryTestRule() { - fun newInstance(location: String? = null, service: String? = null): FirebaseDataConnect = - newInstance(Params(location = location, service = service)) - - override fun createInstance(instanceId: String, params: Params?) = - FirebaseDataConnect.getInstance( - location = params?.location ?: "TestLocation$instanceId", - service = params?.service ?: "TestService$instanceId", - settings = FirebaseDataConnectSettings.emulator + fun newInstance( + serviceId: String? = null, + location: String? = null, + operationSet: String? = null, + revision: String? = null + ): FirebaseDataConnect = + newInstance( + Params( + serviceId = serviceId, + location = location, + operationSet = operationSet, + revision = revision + ) ) + override fun createInstance(params: Params?): FirebaseDataConnect { + val instanceId = Random.nextAlphanumericString() + val serviceConfig = + FirebaseDataConnect.ServiceConfig( + serviceId = params?.serviceId ?: "TestService$instanceId", + location = params?.location ?: "TestLocation$instanceId", + operationSet = params?.operationSet ?: "TestOperationSet$instanceId", + revision = params?.revision ?: "TestRevision$instanceId" + ) + return FirebaseDataConnect.getInstance(serviceConfig, FirebaseDataConnectSettings.emulator) + } + override fun destroyInstance(instance: FirebaseDataConnect) { instance.close() } - data class Params(val location: String? = null, val service: String? = null) + data class Params( + val location: String? = null, + val serviceId: String? = null, + val operationSet: String? = null, + val revision: String? = null + ) } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt index 0cce168bf80..95f1157d43b 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt @@ -4,6 +4,7 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.firebase.initialize +import kotlin.random.Random /** * A JUnit test rule that creates instances of [FirebaseApp] for use during testing, and closes them @@ -11,11 +12,11 @@ import com.google.firebase.initialize */ class TestFirebaseAppFactory : FactoryTestRule() { - override fun createInstance(instanceId: String, params: Nothing?) = + override fun createInstance(params: Nothing?) = Firebase.initialize( Firebase.app.applicationContext, Firebase.app.options, - "test-app-$instanceId" + "test-app-${Random.nextAlphanumericString()}" ) override fun destroyInstance(instance: FirebaseApp) { diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index 33558004a43..e85c8753bb4 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -19,58 +19,72 @@ import com.google.firebase.dataconnect.MutationRef import com.google.firebase.dataconnect.QueryRef import com.google.firebase.dataconnect.mutation import com.google.firebase.dataconnect.query +import com.google.firebase.dataconnect.testutil.TestDataConnectFactory import com.google.firebase.dataconnect.testutil.installEmulatorSchema import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer -class PersonSchema(val dataConnect: FirebaseDataConnect) { +class PersonSchema(private val dataConnect: FirebaseDataConnect) { + + init { + dataConnect.serviceConfig.operationSet.let { + require(it == OPERATION_SET) { + "The given FirebaseDataConnect has operationSet=$it, but expected $OPERATION_SET" + } + } + } suspend fun installEmulatorSchema() { dataConnect.installEmulatorSchema("testing_graphql_schemas/person") } - val createPerson = - dataConnect.mutation( - operationName = "createPerson", - operationSet = "ops", - revision = "42", - ) - - class CreatePersonMutation private constructor() { + object CreatePersonMutation { @Serializable data class PersonData(val id: String, val name: String, val age: Int? = null) @Serializable data class Variables(val data: PersonData) + + suspend fun MutationRef.execute(id: String, name: String, age: Int?) = + execute(Variables(PersonData(id = id, name = name, age = age))) } - val updatePerson = - dataConnect.mutation( - operationName = "updatePerson", - operationSet = "ops", - revision = "42", + val createPerson = + dataConnect.mutation( + operationName = "createPerson", + variablesSerializer = serializer(), + dataDeserializer = serializer() ) - class UpdatePersonMutation private constructor() { + object UpdatePersonMutation { @Serializable data class PersonData(val name: String? = null, val age: Int? = null) @Serializable data class Variables(val id: String, val data: PersonData) + + suspend fun MutationRef.execute( + id: String, + name: String? = null, + age: Int? = null + ) = execute(Variables(id = id, data = PersonData(name = name, age = age))) } - val deletePerson = - dataConnect.mutation( - operationName = "deletePerson", - operationSet = "ops", - revision = "42", + val updatePerson = + dataConnect.mutation( + operationName = "updatePerson", + variablesSerializer = serializer(), + dataDeserializer = serializer() ) - class DeletePersonMutation private constructor() { + object DeletePersonMutation { @Serializable data class Variables(val id: String) + + suspend fun MutationRef.execute(id: String) = execute(Variables(id = id)) } - val getPerson = - dataConnect.query( - operationName = "getPerson", - operationSet = "ops", - revision = "42", + val deletePerson = + dataConnect.mutation( + operationName = "deletePerson", + variablesSerializer = serializer(), + dataDeserializer = serializer() ) - class GetPersonQuery private constructor() { + object GetPersonQuery { @Serializable data class Variables(val id: String, val name: String? = null, val age: Int? = null) @@ -78,65 +92,41 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { data class Data(val person: Person?) { @Serializable data class Person(val name: String, val age: Int? = null) } + + suspend fun QueryRef.execute(id: String) = execute(Variables(id = id)) + + fun QueryRef.subscribe(id: String) = subscribe(Variables(id = id)) } - val getAllPeople = - dataConnect.query( - operationName = "getAllPeople", - operationSet = "ops", - revision = "42", + val getPerson = + dataConnect.query( + operationName = "getPerson", + variablesSerializer = serializer(), + dataDeserializer = serializer() ) - class GetAllPeopleQuery private constructor() { + object GetAllPeopleQuery { @Serializable data class Data(val people: List) { @Serializable data class Person(val id: String, val name: String, val age: Int?) } - } -} -object CreatePersonMutationExt { - suspend fun MutationRef.execute( - id: String, - name: String, - age: Int? - ) = - execute( - PersonSchema.CreatePersonMutation.Variables( - PersonSchema.CreatePersonMutation.PersonData(id = id, name = name, age = age) - ) - ) -} + suspend fun QueryRef.execute() = execute(Unit) -object UpdatePersonMutationExt { - suspend fun MutationRef.execute( - id: String, - name: String? = null, - age: Int? = null - ) = - execute( - PersonSchema.UpdatePersonMutation.Variables( - id = id, - data = PersonSchema.UpdatePersonMutation.PersonData(name = name, age = age) - ) - ) -} - -object DeletePersonMutationExt { - suspend fun MutationRef.execute(id: String) = - execute(PersonSchema.DeletePersonMutation.Variables(id = id)) -} + fun QueryRef.subscribe() = subscribe(Unit) + } -object GetPersonQueryExt { - suspend fun QueryRef - .execute(id: String) = execute(PersonSchema.GetPersonQuery.Variables(id = id)) + val getAllPeople = + dataConnect.query( + operationName = "getAllPeople", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) - fun QueryRef.subscribe( - id: String - ) = subscribe(PersonSchema.GetPersonQuery.Variables(id = id)) -} + companion object { + const val OPERATION_SET = "ops" -object GetAllPeoplePersonQueryExt { - suspend fun QueryRef.execute() = execute(Unit) - fun QueryRef.subscribe() = subscribe(Unit) + fun TestDataConnectFactory.newPersonSchema() = + PersonSchema(newInstance(operationSet = OPERATION_SET)) + } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt index 7c0602a1c5b..59a166cd24b 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt @@ -4,11 +4,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory -import com.google.firebase.dataconnect.testutil.schemas.CreatePersonMutationExt.execute -import com.google.firebase.dataconnect.testutil.schemas.DeletePersonMutationExt.execute -import com.google.firebase.dataconnect.testutil.schemas.GetAllPeoplePersonQueryExt.execute -import com.google.firebase.dataconnect.testutil.schemas.GetPersonQueryExt.execute -import com.google.firebase.dataconnect.testutil.schemas.UpdatePersonMutationExt.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.Companion.newPersonSchema +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.DeletePersonMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetAllPeopleQuery.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Rule @@ -25,7 +26,7 @@ class PersonSchemaTest { @Before fun initializePersonSchema() { - schema = PersonSchema(dataConnectFactory.newInstance()) + schema = dataConnectFactory.newPersonSchema() runBlocking { schema.installEmulatorSchema() } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt index 46e99dedcc2..6d999ec428d 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/BaseRef.kt @@ -20,8 +20,6 @@ abstract class BaseRef internal constructor( val dataConnect: FirebaseDataConnect, internal val operationName: String, - internal val operationSet: String, - internal val revision: String, internal val variablesSerializer: SerializationStrategy, internal val dataDeserializer: DeserializationStrategy, ) { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 227077510ac..228147d54a4 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -31,18 +31,24 @@ import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy internal class DataConnectGrpcClient( - val context: Context, - val projectId: String, - val location: String, - val service: String, - val hostName: String, - val port: Int, - val sslEnabled: Boolean, + context: Context, + projectId: String, + serviceId: String, + location: String, + operationSet: String, + revision: String, + hostName: String, + port: Int, + sslEnabled: Boolean, executor: Executor, creatorLoggerId: String, ) { private val logger = Logger("DataConnectGrpcClient") + private val requestName = + "projects/$projectId/locations/$location/services/$serviceId/" + + "operationSets/$operationSet/revisions/$revision" + init { logger.debug { "Created from $creatorLoggerId" } } @@ -83,15 +89,13 @@ internal class DataConnectGrpcClient( private val grpcStub: DataServiceCoroutineStub by lazy { DataServiceCoroutineStub(grpcChannel) } suspend fun executeQuery( - operationSet: String, operationName: String, - revision: String, variables: VariablesType, variablesSerializer: SerializationStrategy, dataDeserializer: DeserializationStrategy ): DataConnectResult { val request = executeQueryRequest { - this.name = name(operationSet = operationSet, revision = revision) + this.name = requestName this.operationName = operationName this.variables = encodeToStruct(variablesSerializer, variables) } @@ -108,15 +112,13 @@ internal class DataConnectGrpcClient( } suspend fun executeMutation( - operationSet: String, operationName: String, - revision: String, variables: VariablesType, variablesSerializer: SerializationStrategy, dataDeserializer: DeserializationStrategy ): DataConnectResult { val request = executeMutationRequest { - this.name = name(operationSet = operationSet, revision = revision) + this.name = requestName this.operationName = operationName this.variables = encodeToStruct(variablesSerializer, variables) } @@ -132,21 +134,11 @@ internal class DataConnectGrpcClient( ) } - override fun toString(): String { - return "FirebaseDataConnectClient{" + - "projectId=$projectId, location=$location, service=$service, " + - "hostName=$hostName, port=$port, sslEnabled=$sslEnabled}" - } - fun close() { logger.debug { "close() starting" } grpcChannel.shutdownNow() logger.debug { "close() done" } } - - private fun name(operationSet: String, revision: String): String = - "projects/$projectId/locations/$location/services/$service/" + - "operationSets/$operationSet/revisions/$revision" } fun Struct.decode(deserializer: DeserializationStrategy) = diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 075c00940b9..f06e0f1f56b 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -13,6 +13,7 @@ // limitations under the License. package com.google.firebase.dataconnect +import android.annotation.SuppressLint import android.content.Context import com.google.firebase.Firebase import com.google.firebase.FirebaseApp @@ -29,15 +30,13 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.serializer class FirebaseDataConnect internal constructor( private val context: Context, val app: FirebaseApp, private val projectId: String, - val location: String, - val service: String, + val serviceConfig: ServiceConfig, internal val blockingExecutor: Executor, internal val nonBlockingExecutor: Executor, private val creator: FirebaseDataConnectFactory, @@ -48,16 +47,11 @@ internal constructor( Logger("FirebaseDataConnect").apply { debug { "New instance created with " + - "app=${app.name}, projectId=$projectId, location=$location, service=$service" + "app=${app.name}, projectId=$projectId, " + + "serviceConfig=$serviceConfig, settings=$settings" } } - class Queries internal constructor(val dataConnect: FirebaseDataConnect) - val queries = Queries(this) - - class Mutations internal constructor(val dataConnect: FirebaseDataConnect) - val mutations = Mutations(this) - internal val coroutineScope = CoroutineScope( SupervisorJob() + @@ -65,7 +59,7 @@ internal constructor( CoroutineName("FirebaseDataConnect") ) - // Dispatcher used to access `this.closed` and `this.grpcClient` simple. + // Dispatcher used to access `this.closed` and `this.grpcClient`. private val sequentialDispatcher = FirebaseExecutors.newSequentialExecutor(nonBlockingExecutor).asCoroutineDispatcher() @@ -81,8 +75,10 @@ internal constructor( DataConnectGrpcClient( context = context, projectId = projectId, - location = location, - service = service, + serviceId = serviceConfig.serviceId, + location = serviceConfig.location, + revision = serviceConfig.revision, + operationSet = serviceConfig.operationSet, hostName = settings.hostName, port = settings.port, sslEnabled = settings.sslEnabled, @@ -100,8 +96,6 @@ internal constructor( .run { executeQuery( operationName = ref.operationName, - operationSet = ref.operationSet, - revision = ref.revision, variables = variables, variablesSerializer = ref.variablesSerializer, dataDeserializer = ref.dataDeserializer @@ -116,8 +110,6 @@ internal constructor( .run { executeMutation( operationName = ref.operationName, - operationSet = ref.operationSet, - revision = ref.revision, variables = variables, variablesSerializer = ref.variablesSerializer, dataDeserializer = ref.dataDeserializer @@ -140,85 +132,86 @@ internal constructor( creator.remove(this@FirebaseDataConnect) } - override fun toString(): String { - return "FirebaseDataConnect" + - "{app=${app.name}, projectId=$projectId, location=$location, service=$service}" + override fun toString() = + "FirebaseDataConnect{" + + "app=${app.name}, projectId=$projectId, " + + "location=${serviceConfig.location}, serviceId=${serviceConfig.serviceId} " + + "operationSet=${serviceConfig.operationSet}, revision=${serviceConfig.revision}" + + "}" + + class ServiceConfig(serviceId: String, location: String, operationSet: String, revision: String) { + private val impl = + Impl( + serviceId = serviceId, + location = location, + operationSet = operationSet, + revision = revision + ) + + val serviceId: String + get() = impl.serviceId + val location: String + get() = impl.location + val operationSet: String + get() = impl.operationSet + val revision: String + get() = impl.revision + + private data class Impl( + val serviceId: String, + val location: String, + val operationSet: String, + val revision: String + ) + + override fun equals(other: Any?) = + (other as? ServiceConfig)?.let { other.impl == impl } ?: false + + override fun hashCode() = impl.hashCode() + + override fun toString() = + "ServiceConfig(serviceId=$serviceId, location=$location,operationSet=$operationSet, revision=$revision)" } companion object { + @SuppressLint("FirebaseUseExplicitDependencies") fun getInstance( app: FirebaseApp, - location: String, - service: String, - settings: FirebaseDataConnectSettings? = null + serviceConfig: ServiceConfig, + settings: FirebaseDataConnectSettings? = null, ): FirebaseDataConnect = app.get(FirebaseDataConnectFactory::class.java).run { - get(location = location, service = service, settings = settings) + get(serviceConfig = serviceConfig, settings = settings) } fun getInstance( - location: String, - service: String, + serviceConfig: ServiceConfig, settings: FirebaseDataConnectSettings? = null ): FirebaseDataConnect = - getInstance(app = Firebase.app, location = location, service = service, settings = settings) + getInstance(app = Firebase.app, serviceConfig = serviceConfig, settings = settings) } } -inline fun FirebaseDataConnect.query( - operationName: String, - operationSet: String, - revision: String -): QueryRef = - query( - operationName = operationName, - operationSet = operationSet, - revision = revision, - variablesSerializer = serializer(), - dataDeserializer = serializer(), - ) - fun FirebaseDataConnect.query( operationName: String, - operationSet: String, - revision: String, variablesSerializer: SerializationStrategy, dataDeserializer: DeserializationStrategy ): QueryRef = QueryRef( dataConnect = this, operationName = operationName, - operationSet = operationSet, - revision = revision, variablesSerializer = variablesSerializer, dataDeserializer = dataDeserializer ) -inline fun FirebaseDataConnect.mutation( - operationName: String, - operationSet: String, - revision: String, -): MutationRef = - mutation( - operationName = operationName, - operationSet = operationSet, - revision = revision, - variablesSerializer = serializer(), - dataDeserializer = serializer(), - ) - fun FirebaseDataConnect.mutation( operationName: String, - operationSet: String, - revision: String, variablesSerializer: SerializationStrategy, dataDeserializer: DeserializationStrategy ): MutationRef = MutationRef( dataConnect = this, operationName = operationName, - operationSet = operationSet, - revision = revision, variablesSerializer = variablesSerializer, dataDeserializer = dataDeserializer ) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt index 06bf4073687..bf5c5f0e7af 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt @@ -33,8 +33,9 @@ internal class FirebaseDataConnectFactory( } private data class InstanceCacheKey( + val serviceId: String, val location: String, - val service: String, + val operationSet: String, ) private val lock = ReentrantLock() @@ -42,11 +43,13 @@ internal class FirebaseDataConnectFactory( private var closed = false fun get( - location: String, - service: String, + serviceConfig: FirebaseDataConnect.ServiceConfig, settings: FirebaseDataConnectSettings? ): FirebaseDataConnect { - val key = InstanceCacheKey(location = location, service = service) + val key = + serviceConfig.run { + InstanceCacheKey(serviceId = serviceId, location = location, operationSet = operationSet) + } lock.withLock { if (closed) { throw IllegalStateException("FirebaseApp has been deleted") @@ -60,6 +63,14 @@ internal class FirebaseDataConnectFactory( " (cached settings: ${cachedInstance.settings}, specified settings: $settings)" ) } + if (serviceConfig.revision != cachedInstance.serviceConfig.revision) { + throw IllegalArgumentException( + "The cached FirebaseDataConnect instance ($cachedInstance)" + + " must have the same 'revision' as the specified ServiceConfig; however, they are" + + " different (cached revision: ${cachedInstance.serviceConfig.revision}," + + " specified revision: ${serviceConfig.revision})" + ) + } return cachedInstance } @@ -69,8 +80,7 @@ internal class FirebaseDataConnectFactory( context = context, app = firebaseApp, projectId = projectId, - location = location, - service = service, + serviceConfig = serviceConfig, blockingExecutor = blockingExecutor, nonBlockingExecutor = nonBlockingExecutor, creator = this, diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt index 0922d4306f6..0a779cc32c9 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt @@ -20,16 +20,12 @@ class MutationRef internal constructor( dataConnect: FirebaseDataConnect, operationName: String, - operationSet: String, - revision: String, variablesSerializer: SerializationStrategy, dataDeserializer: DeserializationStrategy ) : BaseRef( dataConnect = dataConnect, operationName = operationName, - operationSet = operationSet, - revision = revision, variablesSerializer = variablesSerializer, dataDeserializer = dataDeserializer, ) { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt index 1c4b00590b7..e65d24253f7 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt @@ -20,16 +20,12 @@ class QueryRef internal constructor( dataConnect: FirebaseDataConnect, operationName: String, - operationSet: String, - revision: String, variablesSerializer: SerializationStrategy, dataDeserializer: DeserializationStrategy ) : BaseRef( dataConnect = dataConnect, operationName = operationName, - operationSet = operationSet, - revision = revision, variablesSerializer = variablesSerializer, dataDeserializer = dataDeserializer, ) { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt index eec1fb18635..9fc1b043a79 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/CreatePostMutation.kt @@ -13,12 +13,10 @@ // limitations under the License. package com.google.firebase.dataconnect.generated -import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.MutationRef -import com.google.firebase.dataconnect.mutation import kotlinx.serialization.Serializable -class CreatePostMutation private constructor() { +object CreatePostMutation { @Serializable data class Variables(val data: PostData) { @@ -54,20 +52,8 @@ class CreatePostMutation private constructor() { } } } - - companion object { - fun mutation(dataConnect: FirebaseDataConnect) = - dataConnect.mutation( - operationName = "createPost", - operationSet = "crud", - revision = "1234567890abcdef", - ) - } } -val FirebaseDataConnect.Mutations.createPost - get() = CreatePostMutation.mutation(dataConnect) - suspend fun MutationRef.execute(id: String, content: String) = execute(variablesFor(id = id, content = content)) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt index 8a475b00791..fc7bbd4762b 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -13,13 +13,11 @@ // limitations under the License. package com.google.firebase.dataconnect.generated -import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.QueryRef import com.google.firebase.dataconnect.QuerySubscription -import com.google.firebase.dataconnect.query import kotlinx.serialization.Serializable -class GetPostQuery private constructor() { +object GetPostQuery { @Serializable data class Variables(val id: String) { @@ -44,22 +42,11 @@ class GetPostQuery private constructor() { @Serializable data class Comment(val id: String, val content: String) } } - - companion object { - - fun query(dataConnect: FirebaseDataConnect) = - dataConnect.query( - operationName = "getPost", - operationSet = "crud", - revision = "1234567890abcdef", - ) - } } -typealias GetPostQuerySubscription = QuerySubscription +typealias GetPostQueryRef = QuerySubscription -val FirebaseDataConnect.Queries.getPost - get() = GetPostQuery.query(dataConnect) +typealias GetPostQuerySubscription = QuerySubscription suspend fun QueryRef.execute(id: String) = execute(variablesFor(id = id)) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt new file mode 100644 index 00000000000..68605a7934b --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt @@ -0,0 +1,59 @@ +// Copyright 2023 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.dataconnect.generated + +import com.google.firebase.FirebaseApp +import com.google.firebase.dataconnect.FirebaseDataConnect +import com.google.firebase.dataconnect.FirebaseDataConnectSettings +import com.google.firebase.dataconnect.MutationRef +import com.google.firebase.dataconnect.QueryRef +import com.google.firebase.dataconnect.mutation +import com.google.firebase.dataconnect.query +import kotlinx.serialization.serializer + +class PostsOperationSet( + app: FirebaseApp, + serviceId: String, + location: String, + settings: FirebaseDataConnectSettings, +) { + + val dataConnect: FirebaseDataConnect = + FirebaseDataConnect.getInstance( + app, + FirebaseDataConnect.ServiceConfig( + serviceId = serviceId, + location = location, + operationSet = "crud", + revision = "42" + ), + settings + ) + + val createPost: MutationRef + get() = + dataConnect.mutation( + operationName = "createPost", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + + val getPost: QueryRef + get() = + dataConnect.query( + operationName = "getPost", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectServiceConfigTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectServiceConfigTest.kt new file mode 100644 index 00000000000..786ec0cb74f --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectServiceConfigTest.kt @@ -0,0 +1,327 @@ +package com.google.firebase.dataconnect + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.FirebaseDataConnect.ServiceConfig +import com.google.firebase.dataconnect.testutil.containsWithNonAdjacentText +import org.junit.Test + +private val SAMPLE_SERVICE_ID = "SampleServiceId" +private val SAMPLE_LOCATION = "SampleLocation" +private val SAMPLE_OPERATION_SET = "SampleOperationSet" +private val SAMPLE_REVISION = "SampleRevision" + +class FirebaseDataConnectServiceConfigTest { + + @Test + fun `'serviceId' property should be the same object given to the constructor`() { + val serviceId = "Test Service ID" + val serviceConfig = + ServiceConfig( + serviceId = serviceId, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig.serviceId).isSameInstanceAs(serviceId) + } + + @Test + fun `'location' property should be the same object given to the constructor`() { + val location = "Test Location" + val serviceConfig = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = location, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig.location).isSameInstanceAs(location) + } + + @Test + fun `'operationSet' property should be the same object given to the constructor`() { + val operationSet = "Test Operation Set" + val serviceConfig = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = operationSet, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig.operationSet).isSameInstanceAs(operationSet) + } + + @Test + fun `'revision' property should be the same object given to the constructor`() { + val revision = "Test Revision" + val serviceConfig = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = revision + ) + assertThat(serviceConfig.revision).isSameInstanceAs(revision) + } + + @Test + fun `toString() returns a string that incorporates all property values`() { + val serviceConfig = + ServiceConfig( + serviceId = "MyServiceId", + location = "MyLocation", + operationSet = "MyOperationSet", + revision = "MyRevision" + ) + + val toStringResult = serviceConfig.toString() + + assertThat(toStringResult).startsWith("ServiceConfig(") + assertThat(toStringResult).endsWith(")") + assertThat(toStringResult).containsWithNonAdjacentText("serviceId=MyServiceId") + assertThat(toStringResult).containsWithNonAdjacentText("location=MyLocation") + assertThat(toStringResult).containsWithNonAdjacentText("operationSet=MyOperationSet") + assertThat(toStringResult).containsWithNonAdjacentText("revision=MyRevision") + } + + @Test + fun `equals() should return true for the exact same instance`() { + val serviceConfig = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig.equals(serviceConfig)).isTrue() + } + + @Test + fun `equals() should return true for an equal instance`() { + val serviceConfig1 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + val serviceConfig2 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig1.equals(serviceConfig2)).isTrue() + } + + @Test + fun `equals() should return false for null`() { + val serviceConfig = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig.equals(null)).isFalse() + } + + @Test + fun `equals() should return false for a different type`() { + val serviceConfig = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig.equals(listOf("foo"))).isFalse() + } + + @Test + fun `equals() should return false when only 'serviceId' differs`() { + val serviceConfig1 = + ServiceConfig( + serviceId = "foo", + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + val serviceConfig2 = + ServiceConfig( + serviceId = "bar", + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig1.equals(serviceConfig2)).isFalse() + } + + @Test + fun `equals() should return false when only 'location' differs`() { + val serviceConfig1 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = "foo", + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + val serviceConfig2 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = "bar", + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig1.equals(serviceConfig2)).isFalse() + } + + @Test + fun `equals() should return false when only 'operationSet' differs`() { + val serviceConfig1 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = "foo", + revision = SAMPLE_REVISION + ) + val serviceConfig2 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = "bar", + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig1.equals(serviceConfig2)).isFalse() + } + + @Test + fun `equals() should return false when only 'revision' differs`() { + val serviceConfig1 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = "foo" + ) + val serviceConfig2 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = "bar" + ) + assertThat(serviceConfig1.equals(serviceConfig2)).isFalse() + } + + @Test + fun `hashCode() should return the same value each time it is invoked on a given object`() { + val serviceConfig = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + val hashCode = serviceConfig.hashCode() + assertThat(serviceConfig.hashCode()).isEqualTo(hashCode) + assertThat(serviceConfig.hashCode()).isEqualTo(hashCode) + assertThat(serviceConfig.hashCode()).isEqualTo(hashCode) + } + + @Test + fun `hashCode() should return the same value on equal objects`() { + val serviceConfig1 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + val serviceConfig2 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig1.hashCode()).isEqualTo(serviceConfig2.hashCode()) + } + + @Test + fun `hashCode() should return a different value when only 'serviceId' differs`() { + val serviceConfig1 = + ServiceConfig( + serviceId = "foo", + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + val serviceConfig2 = + ServiceConfig( + serviceId = "bar", + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig1.hashCode()).isNotEqualTo(serviceConfig2.hashCode()) + } + + @Test + fun `hashCode() should return a different value when only 'location' differs`() { + val serviceConfig1 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = "foo", + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + val serviceConfig2 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = "bar", + operationSet = SAMPLE_OPERATION_SET, + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig1.hashCode()).isNotEqualTo(serviceConfig2.hashCode()) + } + + @Test + fun `hashCode() should return a different value when only 'operationSet' differs`() { + val serviceConfig1 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = "foo", + revision = SAMPLE_REVISION + ) + val serviceConfig2 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = "bar", + revision = SAMPLE_REVISION + ) + assertThat(serviceConfig1.hashCode()).isNotEqualTo(serviceConfig2.hashCode()) + } + + @Test + fun `hashCode() should return a different value when only 'revision' differs`() { + val serviceConfig1 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = "foo" + ) + val serviceConfig2 = + ServiceConfig( + serviceId = SAMPLE_SERVICE_ID, + location = SAMPLE_LOCATION, + operationSet = SAMPLE_OPERATION_SET, + revision = "bar" + ) + assertThat(serviceConfig1.hashCode()).isNotEqualTo(serviceConfig2.hashCode()) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt index da6f1b12eb1..16255973350 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt @@ -16,7 +16,7 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat import com.google.firebase.dataconnect.testutil.containsWithNonAdjacentText -import java.util.UUID +import com.google.firebase.dataconnect.testutil.generateRandomAlphanumericString import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -53,7 +53,7 @@ class FirebaseDataConnectSettingsTest { @Test fun `copy() can change the hostName`() { val originalSettings = FirebaseDataConnectSettings.defaults - val newHostName = UUID.randomUUID().toString() + val newHostName = generateRandomAlphanumericString() val modifiedSettings = originalSettings.copy(hostName = newHostName) diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt index c899eb38b73..e8c65dd6d57 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt @@ -2,6 +2,9 @@ package com.google.firebase.dataconnect.testutil import com.google.common.truth.StringSubject import java.util.regex.Pattern +import kotlin.math.abs +import kotlin.math.min +import kotlin.random.Random /** * Asserts that a string contains another string, verifying that the character immediately preceding @@ -11,3 +14,29 @@ import java.util.regex.Pattern */ fun StringSubject.containsWithNonAdjacentText(text: String) = containsMatch("(^|\\W)${Pattern.quote(text)}($|\\W)") + +/** + * Generates and returns a string containing random alphanumeric characters. + * + * @param length the number of random characters to generate and include in the returned string; if + * `null`, then a length of 20 is used. + * @return a string containing the given (or default) number of random alphanumeric characters. + */ +fun Random.nextAlphanumericString(length: Int? = null): String = buildString { + var numCharactersRemaining = + if (length === null) 20 else length.also { require(it >= 0) { "invalid length: $it" } } + + while (numCharactersRemaining > 0) { + // Ignore the first character of the alphanumeric string because its distribution is not random. + val randomCharacters = abs(nextLong()).toAlphaNumericString() + val numCharactersToAppend = min(numCharactersRemaining, randomCharacters.length - 1) + append(randomCharacters, 1, numCharactersToAppend) + numCharactersRemaining -= numCharactersToAppend + } +} + +/** + * Converts this number to a base-36 string, which uses the 26 letters from the English alphabet and + * the 10 numeric digits. + */ +fun Long.toAlphaNumericString(): String = toString(36) diff --git a/firebase-messaging/CHANGELOG.md b/firebase-messaging/CHANGELOG.md index 85a8c2864d9..64b63185e66 100644 --- a/firebase-messaging/CHANGELOG.md +++ b/firebase-messaging/CHANGELOG.md @@ -1,4 +1,6 @@ # Unreleased +* [changed] Called messageHandled() after a message has been handled to indicate + that the message has been handled successfully. * [changed] Added an internal identifier to Firelog logging for compliance. # 23.3.1 diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/FirebaseMessagingService.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/FirebaseMessagingService.java index 31e778b114f..7cc65379bfb 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/FirebaseMessagingService.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/FirebaseMessagingService.java @@ -15,6 +15,7 @@ import static com.google.firebase.messaging.Constants.TAG; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; @@ -22,6 +23,8 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; +import com.google.android.gms.cloudmessaging.CloudMessage; +import com.google.android.gms.cloudmessaging.Rpc; import com.google.firebase.messaging.Constants.MessagePayloadKeys; import com.google.firebase.messaging.Constants.MessageTypes; import java.util.ArrayDeque; @@ -82,6 +85,8 @@ public class FirebaseMessagingService extends EnhancedIntentService { private static final Queue recentlyReceivedMessageIds = new ArrayDeque<>(RECENTLY_RECEIVED_MESSAGE_IDS_MAX_SIZE); + private Rpc rpc; + /** * Called when a message is received. * @@ -173,6 +178,7 @@ private void handleMessageIntent(Intent intent) { if (!alreadyReceivedMessage(messageId)) { passMessageIntentToSdk(intent); } + getRpc(this).messageHandled(new CloudMessage(intent)); } private void passMessageIntentToSdk(Intent intent) { @@ -263,8 +269,20 @@ private String getMessageId(Intent intent) { return messageId; } + private Rpc getRpc(Context context) { + if (rpc == null) { + rpc = new Rpc(context.getApplicationContext()); + } + return rpc; + } + @VisibleForTesting static void resetForTesting() { recentlyReceivedMessageIds.clear(); } + + @VisibleForTesting + void setRpcForTesting(Rpc rpc) { + this.rpc = rpc; + } } diff --git a/firebase-messaging/src/test/java/com/google/firebase/messaging/FirebaseMessagingServiceRoboTest.java b/firebase-messaging/src/test/java/com/google/firebase/messaging/FirebaseMessagingServiceRoboTest.java index 603f48f9427..66915d6fd63 100644 --- a/firebase-messaging/src/test/java/com/google/firebase/messaging/FirebaseMessagingServiceRoboTest.java +++ b/firebase-messaging/src/test/java/com/google/firebase/messaging/FirebaseMessagingServiceRoboTest.java @@ -26,12 +26,14 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.robolectric.Shadows.shadowOf; import android.app.Activity; @@ -53,6 +55,8 @@ import android.os.Process; import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ApplicationProvider; +import com.google.android.gms.cloudmessaging.CloudMessage; +import com.google.android.gms.cloudmessaging.Rpc; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.firebase.FirebaseApp; @@ -74,6 +78,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; @@ -211,6 +216,43 @@ public void testMessage_nullIntentExtras() throws Exception { assertThat(shadowOf(context).getNextStartedService()).isNull(); } + @Test + public void messageHandled() throws Exception { + RemoteMessageBuilder builder = + new RemoteMessageBuilder() + .setFrom(DEFAULT_FROM) + .setTo(DEFAULT_TO) + .addData("key1", "value1") + .setMessageId("message_id_456") + .setSentTime(123456789); + Intent intent = builder.buildIntent(); + Rpc mockRpc = mock(Rpc.class); + service.setRpcForTesting(mockRpc); + CountDownLatch latch = new CountDownLatch(1); + doAnswer( + invocation -> { + latch.await(); + return null; + }) + .when(service) + .onMessageReceived(any(RemoteMessage.class)); + + shadowOf(context).clearStartedServices(); + sendBroadcastToReceiver(intent); + processInternalStartService(context); + + // Shouldn't call messageHandled while onMessageReceived is still running. + verifyNoMoreInteractions(mockRpc); + // Finish onMessageReceived, messageHandled should now be called. + latch.countDown(); + flushTasks(); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(CloudMessage.class); + verify(mockRpc).messageHandled(messageCaptor.capture()); + CloudMessage message = messageCaptor.getValue(); + assertThat(message).isNotNull(); + assertThat(message.getIntent()).isEqualTo(intent); + } + @Test public void testDuplicateMessageDropped() throws Exception { String messageId = "a.message.id"; @@ -308,6 +350,7 @@ public void testOnNewToken() throws Exception { ServiceStarter.getInstance().startMessagingService(context, intent); processInternalStartService(context); + flushTasks(); verify(service).onNewToken("token123"); } @@ -685,6 +728,7 @@ public void startServiceViaReceiver(Intent intent) throws InterruptedException { shadowOf(context).clearStartedServices(); sendBroadcastToReceiver(intent); processInternalStartService(context); + flushTasks(); } /** @@ -700,7 +744,6 @@ public void processInternalStartService(Application application) throws Interrup assertEquals(application.getPackageName(), serviceIntent.getPackage()); service.onStartCommand(serviceIntent, 0 /* flags */, 1 /* startId */); - flushTasks(); } // Flush the Service background tasks diff --git a/subprojects.cfg b/subprojects.cfg index e9120649b18..b0aef2af5e8 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -29,6 +29,7 @@ firebase-database firebase-database:ktx firebase-database-collection firebase-dataconnect +firebase-dataconnect:testutil firebase-datatransport firebase-dynamic-links firebase-dynamic-links:ktx From a6546c041d53a066411aec685ca5dac06279d4d0 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 24 Nov 2023 11:25:09 -0500 Subject: [PATCH 073/573] Sha512Encoder.kt added (#516) --- .../dataconnect/ProtoStructDecoder.kt | 191 ++++----- .../dataconnect/ProtoStructEncoder.kt | 194 +++++---- .../firebase/dataconnect/Sha512Encoder.kt | 374 ++++++++++++++++++ .../FirebaseDataConnectSettingsTest.kt | 5 +- .../dataconnect/ProtoStructDecoderTest.kt | 116 +++--- .../dataconnect/SerializationTestData.kt | 286 ++++++++++++++ .../firebase/dataconnect/Sha512EncoderTest.kt | 164 ++++++++ 7 files changed, 1097 insertions(+), 233 deletions(-) create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512Encoder.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/SerializationTestData.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/Sha512EncoderTest.kt diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt index 95ff6512289..c684a200aa6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt @@ -49,9 +49,21 @@ private fun Value.decode(path: String?, expectedKindCase: KindCase, block: ( private fun Value.decodeBoolean(path: String?): Boolean = decode(path, KindCase.BOOL_VALUE) { it.boolValue } +private fun Value.decodeByte(path: String?): Byte = + decode(path, KindCase.NUMBER_VALUE) { it.numberValue.toInt().toByte() } + +private fun Value.decodeChar(path: String?): Char = + decode(path, KindCase.NUMBER_VALUE) { it.numberValue.toInt().toChar() } + private fun Value.decodeDouble(path: String?): Double = decode(path, KindCase.NUMBER_VALUE) { it.numberValue } +private fun Value.decodeEnum(path: String?): Int = + decode(path, KindCase.NUMBER_VALUE) { it.numberValue.toInt() } + +private fun Value.decodeFloat(path: String?): Float = + decode(path, KindCase.NUMBER_VALUE) { it.numberValue.toFloat() } + private fun Value.decodeString(path: String?): String = decode(path, KindCase.STRING_VALUE) { it.stringValue } @@ -64,7 +76,14 @@ private fun Value.decodeList(path: String?): ListValue = private fun Value.decodeNull(path: String?): NullValue = decode(path, KindCase.NULL_VALUE) { it.nullValue } -private fun Value.decodeInt(path: String?): Int = decodeDouble(path).toInt() +private fun Value.decodeInt(path: String?): Int = + decode(path, KindCase.NUMBER_VALUE) { it.numberValue.toInt() } + +private fun Value.decodeLong(path: String?): Long = + decode(path, KindCase.STRING_VALUE) { it.stringValue.toLong() } + +private fun Value.decodeShort(path: String?): Short = + decode(path, KindCase.NUMBER_VALUE) { it.numberValue.toInt().toShort() } private class ProtoValueDecoder(private val valueProto: Value, private val path: String?) : Decoder { @@ -81,24 +100,23 @@ private class ProtoValueDecoder(private val valueProto: Value, private val path: override fun decodeBoolean() = valueProto.decodeBoolean(path) - override fun decodeByte() = notSupported() + override fun decodeByte() = valueProto.decodeByte(path) - override fun decodeChar() = notSupported() + override fun decodeChar() = valueProto.decodeChar(path) override fun decodeDouble() = valueProto.decodeDouble(path) - override fun decodeEnum(enumDescriptor: SerialDescriptor) = - notSupported("Enum (${enumDescriptor.serialName})") + override fun decodeEnum(enumDescriptor: SerialDescriptor) = valueProto.decodeEnum(path) - override fun decodeFloat() = notSupported() + override fun decodeFloat() = valueProto.decodeFloat(path) override fun decodeInline(descriptor: SerialDescriptor) = ProtoValueDecoder(valueProto, path) override fun decodeInt(): Int = valueProto.decodeInt(path) - override fun decodeLong() = notSupported() + override fun decodeLong() = valueProto.decodeLong(path) - override fun decodeShort() = notSupported() + override fun decodeShort() = valueProto.decodeShort(path) override fun decodeString() = valueProto.decodeString(path) @@ -112,12 +130,6 @@ private class ProtoValueDecoder(private val valueProto: Value, private val path: valueProto.decodeNull(path) return null } - - private companion object { - inline fun notSupported(): Nothing = notSupported(T::class.qualifiedName!!) - fun notSupported(unsupportedTypeName: String): Nothing = - throw SerializationException("decoding $unsupportedTypeName is not supported") - } } private class ProtoStructValueDecoder(private val struct: Struct, private val path: String?) : @@ -145,20 +157,17 @@ private class ProtoStructValueDecoder(private val struct: Struct, private val pa override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(descriptor, index, Value::decodeBoolean) - override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte { - TODO("Not yet implemented") - } + override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(descriptor, index, Value::decodeByte) - override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char { - TODO("Not yet implemented") - } + override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(descriptor, index, Value::decodeChar) - override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int): Double = + override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(descriptor, index, Value::decodeDouble) - override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float { - TODO("Not yet implemented") - } + override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(descriptor, index, Value::decodeFloat) override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(descriptor, index, ::ProtoValueDecoder) @@ -166,15 +175,13 @@ private class ProtoStructValueDecoder(private val struct: Struct, private val pa override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(descriptor, index, Value::decodeInt) - override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long { - TODO("Not yet implemented") - } + override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(descriptor, index, Value::decodeLong) - override fun decodeShortElement(descriptor: SerialDescriptor, index: Int): Short { - TODO("Not yet implemented") - } + override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(descriptor, index, Value::decodeShort) - override fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String = + override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(descriptor, index, Value::decodeString) private fun decodeValueElement( @@ -184,12 +191,35 @@ private class ProtoStructValueDecoder(private val struct: Struct, private val pa ): T { val elementName = descriptor.getElementName(index) val elementPath = elementPathForName(elementName) - val value = + val elementKind = descriptor.getElementDescriptor(index).kind + + val valueProto = + struct.fieldsMap[elementName] + ?: throw SerializationException("element \"$elementPath\" missing (expected $elementKind)") + + return block(valueProto, elementPath) + } + + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T { + if (previousValue !== null) { + return previousValue + } + + val elementName = descriptor.getElementName(index) + val elementPath = elementPathForName(elementName) + val elementKind = descriptor.getElementDescriptor(index).kind + + val valueProto = struct.fieldsMap[elementName] - ?: throw SerializationException( - "element \"$elementPath\" missing (expected ${descriptor.getElementDescriptor(index).kind})" - ) - return block(value, elementPath) + ?: if (elementKind is StructureKind.OBJECT) Value.getDefaultInstance() + else throw SerializationException("element \"$elementPath\" missing; expected $elementKind") + + return deserializer.deserialize(ProtoValueDecoder(valueProto, elementPath)) } @ExperimentalSerializationApi @@ -211,28 +241,6 @@ private class ProtoStructValueDecoder(private val struct: Struct, private val pa } } - override fun decodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy, - previousValue: T? - ): T { - if (previousValue !== null) { - return previousValue - } - - val elementName = descriptor.getElementName(index) - val elementPath = elementPathForName(elementName) - val valueProto = struct.fieldsMap[elementName] - if (valueProto === null) { - throw SerializationException( - "element \"$elementPath\" missing; expected ${descriptor.getElementDescriptor(index).kind}" - ) - } - - return deserializer.deserialize(ProtoValueDecoder(valueProto, elementPath)) - } - private fun elementPathForName(elementName: String) = if (path === null) elementName else "${path}.${elementName}" } @@ -253,20 +261,17 @@ private class ProtoListValueDecoder(private val list: ListValue, private val pat override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(index, Value::decodeBoolean) - override fun decodeByteElement(descriptor: SerialDescriptor, index: Int): Byte { - TODO("Not yet implemented") - } + override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeByte) - override fun decodeCharElement(descriptor: SerialDescriptor, index: Int): Char { - TODO("Not yet implemented") - } + override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeChar) override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(index, Value::decodeDouble) - override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int): Float { - TODO("Not yet implemented") - } + override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeFloat) override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(index, ::ProtoValueDecoder) @@ -274,13 +279,11 @@ private class ProtoListValueDecoder(private val list: ListValue, private val pat override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(index, Value::decodeInt) - override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long { - TODO("Not yet implemented") - } + override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeLong) - override fun decodeShortElement(descriptor: SerialDescriptor, index: Int): Short { - TODO("Not yet implemented") - } + override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) = + decodeValueElement(index, Value::decodeShort) override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = decodeValueElement(index, Value::decodeString) @@ -288,6 +291,18 @@ private class ProtoListValueDecoder(private val list: ListValue, private val pat private inline fun decodeValueElement(index: Int, block: Value.(String?) -> T): T = block(list.valuesList[index], elementPathForIndex(index)) + override fun decodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + previousValue: T? + ): T = + if (previousValue !== null) previousValue + else + deserializer.deserialize( + ProtoValueDecoder(list.valuesList[index], elementPathForIndex(index)) + ) + @ExperimentalSerializationApi override fun decodeNullableSerializableElement( descriptor: SerialDescriptor, @@ -304,18 +319,6 @@ private class ProtoListValueDecoder(private val list: ListValue, private val pat } } - override fun decodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy, - previousValue: T? - ): T = - if (previousValue !== null) previousValue - else - deserializer.deserialize( - ProtoValueDecoder(list.valuesList[index], elementPathForIndex(index)) - ) - private fun elementPathForIndex(index: Int) = if (path === null) "[$index]" else "${path}[$index]" } @@ -338,25 +341,25 @@ private object ProtoObjectValueDecoder : CompositeDecoder { override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) = notSupported() - @ExperimentalSerializationApi - override fun decodeNullableSerializableElement( + override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = notSupported() + + override fun decodeSerializableElement( descriptor: SerialDescriptor, index: Int, - deserializer: DeserializationStrategy, + deserializer: DeserializationStrategy, previousValue: T? ) = notSupported() - override fun decodeSerializableElement( + @ExperimentalSerializationApi + override fun decodeNullableSerializableElement( descriptor: SerialDescriptor, index: Int, - deserializer: DeserializationStrategy, + deserializer: DeserializationStrategy, previousValue: T? ) = notSupported() - override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) = notSupported() - - override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = notSupported() - private fun notSupported(): Nothing = throw UnsupportedOperationException( "The only valid method calls on ProtoObjectValueDecoder are " + diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt index 407bb77b0db..830102eabda 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt @@ -20,130 +20,165 @@ import kotlinx.serialization.serializer inline fun encodeToStruct(value: T): Struct = encodeToStruct(serializer(), value) fun encodeToStruct(serializer: SerializationStrategy, value: T): Struct { - val values = ProtoValueEncoder().apply { encodeSerializableValue(serializer, value) }.values + val values = mutableListOf() + ProtoValueEncoder(path = null, onValue = values::add).encodeSerializableValue(serializer, value) if (values.isEmpty()) { return Struct.getDefaultInstance() } require(values.size == 1) { - "encoding produced ${values.size} Value objects, " + "but expected at most 1" + "encoding produced ${values.size} Value objects, but expected either 0 or 1" } - val value = values.first() - require(value.hasStructValue()) { - "encoding produced ${value.kindCase}, but expected ${KindCase.STRUCT_VALUE}" + val valueProto = values.single() + require(valueProto.hasStructValue()) { + "encoding produced ${valueProto.kindCase}, but expected ${KindCase.STRUCT_VALUE}" } - return value.structValue + return valueProto.structValue } -private class ProtoValueEncoder : Encoder { +private fun Boolean.toProtoValue(): Value = Value.newBuilder().setBoolValue(this).build() - val values = mutableListOf() +private fun Byte.toProtoValue(): Value = toInt().toProtoValue() + +private fun Char.toProtoValue(): Value = code.toProtoValue() + +private fun Double.toProtoValue(): Value = Value.newBuilder().setNumberValue(this).build() + +private fun Float.toProtoValue(): Value = toDouble().toProtoValue() + +private fun Int.toProtoValue(): Value = toDouble().toProtoValue() + +private fun Long.toProtoValue(): Value = toString().toProtoValue() + +private fun Short.toProtoValue(): Value = toInt().toProtoValue() + +private fun String.toProtoValue(): Value = Value.newBuilder().setStringValue(this).build() + +private val nullProtoValue: Value + get() { + return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build() + } + +private class ProtoValueEncoder(private val path: String?, private val onValue: (Value) -> Unit) : + Encoder { override val serializersModule = EmptySerializersModule() override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder = when (val kind = descriptor.kind) { is StructureKind.MAP, - is StructureKind.CLASS -> ProtoStructValueEncoder(values) - is StructureKind.LIST -> ProtoListValueEncoder(values) + is StructureKind.CLASS -> ProtoStructValueEncoder(path, onValue) + is StructureKind.LIST -> ProtoListValueEncoder(path, onValue) is StructureKind.OBJECT -> ProtoObjectValueEncoder else -> throw IllegalArgumentException("unsupported SerialKind: ${kind::class.qualifiedName}") } override fun encodeBoolean(value: Boolean) { - values.add(Value.newBuilder().setBoolValue(value).build()) + onValue(value.toProtoValue()) } override fun encodeByte(value: Byte) { - TODO("Not yet implemented") + onValue(value.toProtoValue()) } override fun encodeChar(value: Char) { - TODO("Not yet implemented") + onValue(value.toProtoValue()) } override fun encodeDouble(value: Double) { - values.add(Value.newBuilder().setNumberValue(value).build()) + onValue(value.toProtoValue()) } override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { - TODO("Not yet implemented") + onValue(index.toProtoValue()) } override fun encodeFloat(value: Float) { - TODO("Not yet implemented") + onValue(value.toProtoValue()) } override fun encodeInline(descriptor: SerialDescriptor) = this override fun encodeInt(value: Int) { - encodeDouble(value.toDouble()) + onValue(value.toProtoValue()) } override fun encodeLong(value: Long) { - TODO("Not yet implemented") + onValue(value.toProtoValue()) } @ExperimentalSerializationApi override fun encodeNull() { - values.add(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()) + onValue(nullProtoValue) + } + + @ExperimentalSerializationApi + override fun encodeNotNullMark() { + encodeBoolean(true) } override fun encodeShort(value: Short) { - TODO("Not yet implemented") + onValue(value.toProtoValue()) } override fun encodeString(value: String) { - values.add(Value.newBuilder().setStringValue(value).build()) + onValue(value.toProtoValue()) } } -private abstract class ProtoCompositeValueEncoder(private val dest: MutableList) : - CompositeEncoder { +private abstract class ProtoCompositeValueEncoder( + private val path: String?, + private val onValue: (Value) -> Unit +) : CompositeEncoder { override val serializersModule = EmptySerializersModule() - private val valueEncoder = ProtoValueEncoder() - private val keys = mutableListOf() + private val valueByKey = mutableMapOf() + + private fun putValue(descriptor: SerialDescriptor, index: Int, value: Value) { + val key = keyOf(descriptor, index) + valueByKey[key] = value + } protected abstract fun keyOf(descriptor: SerialDescriptor, index: Int): K + protected abstract fun formattedKeyForElementPath(key: K): String override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeBoolean(value) + putValue(descriptor, index, value.toProtoValue()) } override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeByte(value) + putValue(descriptor, index, value.toProtoValue()) } override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeChar(value) + putValue(descriptor, index, value.toProtoValue()) } override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeDouble(value) + putValue(descriptor, index, value.toProtoValue()) } override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeFloat(value) + putValue(descriptor, index, value.toProtoValue()) } override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { - keys.add(keyOf(descriptor, index)) - return valueEncoder.encodeInline(descriptor) + throw UnsupportedOperationException("inline is not implemented yet") } override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeInt(value) + putValue(descriptor, index, value.toProtoValue()) } override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeLong(value) + putValue(descriptor, index, value.toProtoValue()) + } + + override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { + putValue(descriptor, index, value.toProtoValue()) + } + + override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { + putValue(descriptor, index, value.toProtoValue()) } @ExperimentalSerializationApi @@ -153,8 +188,9 @@ private abstract class ProtoCompositeValueEncoder(private val dest: MutableLi serializer: SerializationStrategy, value: T? ) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeNullableSerializableValue(serializer, value) + val key = keyOf(descriptor, index) + val encoder = ProtoValueEncoder(elementPathForKey(key)) { valueByKey[key] = it } + encoder.encodeNullableSerializableValue(serializer, value) } override fun encodeSerializableElement( @@ -163,50 +199,47 @@ private abstract class ProtoCompositeValueEncoder(private val dest: MutableLi serializer: SerializationStrategy, value: T ) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeSerializableValue(serializer, value) - } - - override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeShort(value) - } - - override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { - keys.add(keyOf(descriptor, index)) - valueEncoder.encodeString(value) + val key = keyOf(descriptor, index) + val encoder = ProtoValueEncoder(elementPathForKey(key)) { valueByKey[key] = it } + encoder.encodeSerializableValue(serializer, value) } override fun endStructure(descriptor: SerialDescriptor) { - require(valueEncoder.values.size == keys.size) { - "internal error: " + - "valueEncoder.values.size != keys.size " + - "(${valueEncoder.values.size} != ${keys.size})" - } - - val valueByKey = - buildMap { - this@ProtoCompositeValueEncoder.keys.forEachIndexed { valueEncoderIndex, destKey -> - put(destKey, valueEncoder.values[valueEncoderIndex]) - } - } - - dest.add(Value.newBuilder().also { populate(it, valueByKey) }.build()) + onValue(Value.newBuilder().also { populate(descriptor, it, valueByKey) }.build()) } - protected abstract fun populate(valueBuilder: Value.Builder, valueByKey: Map) + private fun elementPathForKey(key: K): String = + formattedKeyForElementPath(key).let { if (path === null) it else "$path$it" } + + protected abstract fun populate( + descriptor: SerialDescriptor, + valueBuilder: Value.Builder, + valueByKey: Map + ) } -private class ProtoListValueEncoder(dest: MutableList) : - ProtoCompositeValueEncoder(dest) { +private class ProtoListValueEncoder(private val path: String?, onValue: (Value) -> Unit) : + ProtoCompositeValueEncoder(path, onValue) { + override fun keyOf(descriptor: SerialDescriptor, index: Int) = index - override fun populate(valueBuilder: Value.Builder, valueByKey: Map) { + override fun formattedKeyForElementPath(key: Int) = "[$key]" + + override fun populate( + descriptor: SerialDescriptor, + valueBuilder: Value.Builder, + valueByKey: Map + ) { valueBuilder.setListValue( ListValue.newBuilder().also { listValueBuilder -> for (i in 0 until valueByKey.size) { listValueBuilder.addValues( - valueByKey[i] ?: throw SerializationException("list value missing at index $i") + valueByKey[i] + ?: throw SerializationException( + "$path: list value missing at index $i" + + " (have ${valueByKey.size} indexes:" + + " ${valueByKey.keys.sorted().joinToString()})" + ) ) } } @@ -214,11 +247,18 @@ private class ProtoListValueEncoder(dest: MutableList) : } } -private class ProtoStructValueEncoder(dest: MutableList) : - ProtoCompositeValueEncoder(dest) { +private class ProtoStructValueEncoder(path: String?, onValue: (Value) -> Unit) : + ProtoCompositeValueEncoder(path, onValue) { + override fun keyOf(descriptor: SerialDescriptor, index: Int) = descriptor.getElementName(index) - override fun populate(valueBuilder: Value.Builder, valueByKey: Map) { + override fun formattedKeyForElementPath(key: String) = ".$key" + + override fun populate( + descriptor: SerialDescriptor, + valueBuilder: Value.Builder, + valueByKey: Map + ) { valueBuilder.setStructValue( Struct.newBuilder().also { structBuilder -> valueByKey.forEach { (key, value) -> diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512Encoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512Encoder.kt new file mode 100644 index 00000000000..781321469be --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512Encoder.kt @@ -0,0 +1,374 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package com.google.firebase.dataconnect + +import java.io.DataOutputStream +import java.io.OutputStream +import java.security.DigestOutputStream +import java.security.MessageDigest +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.serializer + +inline fun calculateSha512(value: T) = calculateSha512(serializer(), value) + +fun calculateSha512(serializer: SerializationStrategy, value: T): ByteArray = + Sha512Encoder().apply { encodeSerializableValue(serializer, value) }.digest.digest() + +private class Sha512Encoder : Encoder { + + val digest = newSha512MessageDigest() + val out = DataOutputStream(DigestOutputStream(NullOutputStream, digest)) + + override val serializersModule = EmptySerializersModule() + + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + return when (val kind = descriptor.kind) { + is StructureKind.LIST -> { + out.writeTag(Tag.ListBegin) + Sha512ListEncoder(this, endTag = Tag.ListEnd) + } + is StructureKind.MAP -> { + out.writeTag(Tag.MapBegin) + Sha512MapEncoder(this, endTag = Tag.MapEnd) + } + is StructureKind.CLASS -> { + out.writeTag(Tag.ClassBegin) + Sha512MapEncoder(this, endTag = Tag.ClassEnd) + } + is StructureKind.OBJECT -> { + out.writeTag(Tag.ObjectBegin) + Sha512ObjectEncoder(this, endTag = Tag.ObjectEnd) + } + else -> throw SerializationException("beginStructure unexpected SerialDescriptor.kind: $kind") + } + } + + override fun encodeBoolean(value: Boolean) { + out.writeTag(Tag.Boolean) + out.writeBoolean(value) + } + + override fun encodeByte(value: Byte) { + out.writeTag(Tag.Byte) + out.writeByte(value.toInt()) + } + + override fun encodeChar(value: Char) { + out.writeTag(Tag.Char) + out.writeChar(value.code) + } + + override fun encodeDouble(value: Double) { + out.writeTag(Tag.Double) + out.writeDouble(value) + } + + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { + out.writeTag(Tag.Enum) + out.writeInt(index) + } + + override fun encodeFloat(value: Float) { + out.writeTag(Tag.Float) + out.writeFloat(value) + } + + override fun encodeInline(descriptor: SerialDescriptor): Encoder { + out.writeTag(Tag.Inline) + return this + } + + override fun encodeInt(value: Int) { + out.writeTag(Tag.Int) + out.writeInt(value) + } + + override fun encodeLong(value: Long) { + out.writeTag(Tag.Long) + out.writeLong(value) + } + + @ExperimentalSerializationApi + override fun encodeNull() { + out.writeTag(Tag.Null) + } + + override fun encodeShort(value: Short) { + out.writeTag(Tag.Short) + out.writeShort(value.toInt()) + } + + override fun encodeString(value: String) { + out.writeTag(Tag.String) + out.writeUTF(value) + } +} + +private class Sha512ListEncoder(val parentEncoder: Sha512Encoder, val endTag: Tag) : + CompositeEncoder { + + override val serializersModule = EmptySerializersModule() + + private val elements = mutableMapOf() + + override fun endStructure(descriptor: SerialDescriptor) { + parentEncoder.out.run { + val digestBytes = ByteArray(parentEncoder.digest.digestLength) + var expectedNextIndex = 0 + elements.entries + .sortedBy { (index, _) -> index } + .forEach { (index, encoder) -> + if (index != expectedNextIndex) { + throw SerializationException("got index $index, but expected $expectedNextIndex") + } + expectedNextIndex++ + writeInt(index) + encoder.digest.digest(digestBytes, 0, digestBytes.size) + write(digestBytes) + } + writeTag(endTag) + } + } + + override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { + elements[index] = Sha512Encoder().apply { encodeBoolean(value) } + } + + override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { + elements[index] = Sha512Encoder().apply { encodeByte(value) } + } + + override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { + elements[index] = Sha512Encoder().apply { encodeChar(value) } + } + + override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { + elements[index] = Sha512Encoder().apply { encodeDouble(value) } + } + + override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { + elements[index] = Sha512Encoder().apply { encodeFloat(value) } + } + + override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { + return Sha512Encoder().also { elements[index] = it } + } + + override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { + elements[index] = Sha512Encoder().apply { encodeInt(value) } + } + + override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { + elements[index] = Sha512Encoder().apply { encodeLong(value) } + } + + override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { + elements[index] = Sha512Encoder().apply { encodeShort(value) } + } + + override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { + elements[index] = Sha512Encoder().apply { encodeString(value) } + } + + override fun encodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T + ) { + elements[index] = Sha512Encoder().apply { encodeSerializableValue(serializer, value) } + } + + @ExperimentalSerializationApi + override fun encodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T? + ) { + elements[index] = Sha512Encoder().apply { encodeNullableSerializableValue(serializer, value) } + } +} + +private class Sha512MapEncoder(val parentEncoder: Sha512Encoder, val endTag: Tag) : + CompositeEncoder { + + override val serializersModule = EmptySerializersModule() + + private val elements = mutableMapOf() + + override fun endStructure(descriptor: SerialDescriptor) { + parentEncoder.out.run { + val digestBytes = ByteArray(parentEncoder.digest.digestLength) + elements.entries + .sortedBy { (key, _) -> key } + .forEach { (key, encoder) -> + writeUTF(key) + encoder.digest.digest(digestBytes, 0, digestBytes.size) + write(digestBytes) + } + writeTag(endTag) + } + } + + override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { + elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeBoolean(value) } + } + + override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { + elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeByte(value) } + } + + override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { + elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeChar(value) } + } + + override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { + elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeDouble(value) } + } + + override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { + elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeFloat(value) } + } + + override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { + return Sha512Encoder().also { elements[descriptor.getElementName(index)] = it } + } + + override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { + elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeInt(value) } + } + + override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { + elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeLong(value) } + } + + override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { + elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeShort(value) } + } + + override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { + elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeString(value) } + } + + override fun encodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T + ) { + elements[descriptor.getElementName(index)] = + Sha512Encoder().apply { encodeSerializableValue(serializer, value) } + } + + @ExperimentalSerializationApi + override fun encodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T? + ) { + elements[descriptor.getElementName(index)] = + Sha512Encoder().apply { encodeNullableSerializableValue(serializer, value) } + } +} + +private class Sha512ObjectEncoder(val parentEncoder: Sha512Encoder, val endTag: Tag) : + CompositeEncoder { + + override val serializersModule = EmptySerializersModule() + + override fun endStructure(descriptor: SerialDescriptor) { + parentEncoder.out.writeTag(endTag) + } + + override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) = + notSupported() + + override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) = + notSupported() + + override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) = + notSupported() + + override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = + notSupported() + + override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) = + notSupported() + + override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder = + notSupported() + + override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) = + notSupported() + + override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) = + notSupported() + + override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) = + notSupported() + + override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) = + notSupported() + + override fun encodeSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T + ) = notSupported() + + @ExperimentalSerializationApi + override fun encodeNullableSerializableElement( + descriptor: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T? + ) = notSupported() + + private fun notSupported(): Nothing = + throw UnsupportedOperationException( + "The only valid method call on Sha512ObjectEncoder is endStructure()" + ) +} + +private enum class Tag { + ListBegin, + ListEnd, + MapBegin, + MapEnd, + ClassBegin, + ClassEnd, + ObjectBegin, + ObjectEnd, + Boolean, + Byte, + Char, + Double, + Enum, + Float, + Inline, + Int, + Long, + Null, + Short, + String +} + +private fun DataOutputStream.writeTag(tag: Tag) = writeInt(tag.ordinal) + +private object NullOutputStream : OutputStream() { + override fun write(b: Int) {} + override fun write(b: ByteArray?) {} + override fun write(b: ByteArray?, off: Int, len: Int) {} +} + +private fun newSha512MessageDigest(): MessageDigest = MessageDigest.getInstance("SHA-512") diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt index 16255973350..4261aa08d74 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt @@ -16,7 +16,8 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat import com.google.firebase.dataconnect.testutil.containsWithNonAdjacentText -import com.google.firebase.dataconnect.testutil.generateRandomAlphanumericString +import com.google.firebase.dataconnect.testutil.nextAlphanumericString +import kotlin.random.Random import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -53,7 +54,7 @@ class FirebaseDataConnectSettingsTest { @Test fun `copy() can change the hostName`() { val originalSettings = FirebaseDataConnectSettings.defaults - val newHostName = generateRandomAlphanumericString() + val newHostName = Random.nextAlphanumericString() val modifiedSettings = originalSettings.copy(hostName = newHostName) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt index 2c29350b749..1ff9e11a968 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt @@ -33,6 +33,49 @@ import org.junit.Test class ProtoStructDecoderTest { + @Test + fun `decodeFromStruct() can encode and decode a complex object A`() { + val obj = + SerializationTestData.AllTheTypes.newInstance(seed = "TheQuickBrown").withEmptyUnitLists() + val struct = encodeToStruct(obj) + val decodedObj = decodeFromStruct(struct) + assertThat(decodedObj).isEqualTo(obj) + } + + @Test + fun `decodeFromStruct() can encode and decode a complex object B`() { + val obj = + SerializationTestData.AllTheTypes.newInstance(seed = "FoxJumpsOver").withEmptyUnitLists() + val struct = encodeToStruct(obj) + val decodedObj = decodeFromStruct(struct) + assertThat(decodedObj).isEqualTo(obj) + } + + @Test + fun `decodeFromStruct() can encode and decode a complex object C`() { + val obj = + SerializationTestData.AllTheTypes.newInstance(seed = "TheLazyDog").withEmptyUnitLists() + val struct = encodeToStruct(obj) + val decodedObj = decodeFromStruct(struct) + assertThat(decodedObj).isEqualTo(obj) + } + + @Test + fun `decodeFromStruct() can encode and decode a list of nullable Unit ending in null`() { + @Serializable data class TestData(val list: List) + val struct = encodeToStruct(TestData(listOf(null, Unit, null))) + val decodedObj = decodeFromStruct(struct) + assertThat(decodedObj).isEqualTo(TestData(listOf(null, Unit, null))) + } + + @Test + fun `decodeFromStruct() can encode and decode a list of nullable Unit ending in Unit`() { + @Serializable data class TestData(val list: List) + val struct = encodeToStruct(TestData(listOf(null, Unit, null, Unit))) + val decodedObj = decodeFromStruct(struct) + assertThat(decodedObj).isEqualTo(TestData(listOf(null, Unit, null, Unit))) + } + @Test fun `decodeFromStruct() can decode a Struct to Unit`() { val decodedTestData = decodeFromStruct(Struct.getDefaultInstance()) @@ -509,53 +552,6 @@ class ProtoStructDecoderTest { ) } - @Test - fun `decodeFromStruct() should throw SerializationException if attempting to decode a Byte`() { - assertThrowsNotSupported() - } - - @Test - fun `decodeFromStruct() should throw SerializationException if attempting to decode a Char`() { - assertThrowsNotSupported() - } - - @Test - fun `decodeFromStruct() should throw SerializationException if attempting to decode a Enum`() { - assertThrowsNotSupported() - } - - @Test - fun `decodeFromStruct() should throw SerializationException if attempting to decode a Float`() { - assertThrowsNotSupported() - } - - @Test - fun `decodeFromStruct() should throw SerializationException if attempting to decode an Inline of supported type`() { - assertDecodeFromStructThrowsIncorrectKindCase( - expectedKind = KindCase.STRING_VALUE, - actualKind = KindCase.STRUCT_VALUE - ) - assertDecodeFromStructThrowsIncorrectKindCase( - expectedKind = KindCase.NUMBER_VALUE, - actualKind = KindCase.STRUCT_VALUE - ) - } - - @Test - fun `decodeFromStruct() should throw SerializationException if attempting to decode an Inline of _unsupported_ type`() { - assertThrowsNotSupported(expectedTypeInMessage = Byte::class) - } - - @Test - fun `decodeFromStruct() should throw SerializationException if attempting to decode a Long`() { - assertThrowsNotSupported() - } - - @Test - fun `decodeFromStruct() should throw SerializationException if attempting to decode a Short`() { - assertThrowsNotSupported() - } - @Test fun `decodeFromStruct() should throw SerializationException if decoding a Boolean value found a different type`() { @Serializable data class TestEncodeSubData(val someValue: String) @@ -673,6 +669,19 @@ class ProtoStructDecoderTest { ) } + private enum class TestEnum { + A, + B, + C, + D + } + + @Serializable @JvmInline private value class TestStringValueClass(val a: String) + + @Serializable @JvmInline private value class TestIntValueClass(val a: Int) + + @Serializable @JvmInline private value class TestByteValueClass(val a: Byte) + // TODO: Add tests for decoding to objects with unsupported field types (e.g. Byte, Char) and // list elements of unsupported field types (e.g. Byte, Char). @@ -729,16 +738,3 @@ private inline fun assertThrowsNotSupported( ) ) } - -private enum class TestEnum { - A, - B, - C, - D -} - -@Serializable @JvmInline value class TestStringValueClass(val a: String) - -@Serializable @JvmInline value class TestIntValueClass(val a: Int) - -@Serializable @JvmInline value class TestByteValueClass(val a: Byte) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/SerializationTestData.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/SerializationTestData.kt new file mode 100644 index 00000000000..b037b584ab1 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/SerializationTestData.kt @@ -0,0 +1,286 @@ +// Copyright 2023 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.dataconnect + +import kotlin.math.PI +import kotlin.math.abs +import kotlinx.serialization.Serializable + +object SerializationTestData { + + enum class TestEnum { + A, + B, + C, + D, + } + + @Serializable @JvmInline value class TestStringValueClass(val a: String) + + @Serializable @JvmInline value class TestIntValueClass(val a: Int) + + @Serializable + data class TestData1(val s: String, val i: Int) { + companion object { + fun newInstance(seed: String = "abcdef01234567890"): TestData1 = + seed.run { TestData1(s = seededString("s"), i = seededInt("i")) } + } + } + + @Serializable + data class TestData2(val td: TestData1, val ntd: TestData1?, val noll: TestData1?) { + companion object { + fun newInstance(seed: String = "abcdef01234567890"): TestData2 = + TestData2( + td = TestData1.newInstance(seed + "td"), + ntd = TestData1.newInstance(seed + "ntd"), + noll = null + ) + } + } + + @Serializable + data class AllTheTypes( + val boolean: Boolean, + val byte: Byte, + val char: Char, + val double: Double, + val doubleMinValue: Double, + val doubleMaxValue: Double, + val doubleNegativeInfinity: Double, + val doublePositiveInfinity: Double, + val doubleNaN: Double, + val enum: TestEnum, + val float: Float, + val floatMinValue: Float, + val floatMaxValue: Float, + val floatNegativeInfinity: Float, + val floatPositiveInfinity: Float, + val floatNaN: Float, + val inlineString: TestStringValueClass, + val inlineInt: TestIntValueClass, + val int: Int, + val long: Long, + val noll: Unit?, + val short: Short, + val string: String, + val testData: TestData2, + val booleanList: List, + val byteList: List, + val charList: List, + val doubleList: List, + val enumList: List, + val floatList: List, + val inlineStringList: List, + val inlineIntList: List, + val intList: List, + val longList: List, + val shortList: List, + val stringList: List, + val testDataList: List, + val booleanNull: Boolean?, + val byteNull: Byte?, + val charNull: Char?, + val doubleNull: Double?, + val enumNull: TestEnum?, + val floatNull: Float?, + val inlineStringNull: TestStringValueClass?, + val inlineIntNull: TestIntValueClass?, + val intNull: Int?, + val longNull: Long?, + val shortNull: Short?, + val stringNull: String?, + val testDataNull: TestData2?, + val booleanNullable: Boolean?, + val byteNullable: Byte?, + val charNullable: Char?, + val doubleNullable: Double?, + val enumNullable: TestEnum?, + val floatNullable: Float?, + val inlineStringNullable: TestStringValueClass?, + val inlineIntNullable: TestIntValueClass?, + val intNullable: Int?, + val longNullable: Long?, + val shortNullable: Short?, + val stringNullable: String?, + val testDataNullable: TestData2?, + val booleanNullableList: List, + val byteNullableList: List, + val charNullableList: List, + val doubleNullableList: List, + val enumNullableList: List, + val floatNullableList: List, + val inlineStringNullableList: List, + val inlineIntNullableList: List, + val intNullableList: List, + val longNullableList: List, + val shortNullableList: List, + val stringNullableList: List, + val testDataNullableList: List, + val nested: AllTheTypes?, + val unit: Unit, + val nullUnit: Unit?, + val nullableUnit: Unit?, + val listOfUnit: List, + val listOfNullableUnit: List, + ) { + companion object { + + fun newInstance(seed: String = "abcdef01234567890", nesting: Int = 1): AllTheTypes = + seed.run { + AllTheTypes( + boolean = seededBoolean("plain"), + byte = seededByte("plain"), + char = seededChar("plain"), + double = seededDouble("plain"), + doubleMinValue = Double.MIN_VALUE, + doubleMaxValue = Double.MAX_VALUE, + doubleNegativeInfinity = Double.POSITIVE_INFINITY, + doublePositiveInfinity = Double.NEGATIVE_INFINITY, + doubleNaN = Double.NaN, + enum = seededEnum("plain"), + float = seededFloat("plain"), + floatMinValue = Float.MIN_VALUE, + floatMaxValue = Float.MAX_VALUE, + floatNegativeInfinity = Float.POSITIVE_INFINITY, + floatPositiveInfinity = Float.NEGATIVE_INFINITY, + floatNaN = Float.NaN, + inlineString = TestStringValueClass(seededString("value")), + inlineInt = TestIntValueClass(seededInt("value")), + int = seededInt("plain"), + long = seededLong("plain"), + noll = null, + short = seededShort("plain"), + string = seededString("plain"), + testData = TestData2.newInstance(seededString("plain")), + booleanList = listOf(seededBoolean("list0"), seededBoolean("list1")), + byteList = listOf(seededByte("list0"), seededByte("list1")), + charList = listOf(seededChar("list0"), seededChar("list1")), + doubleList = listOf(seededDouble("list0"), seededDouble("list1")), + enumList = listOf(seededEnum("list0"), seededEnum("list1")), + floatList = listOf(seededFloat("list0"), seededFloat("list1")), + inlineStringList = + listOf( + TestStringValueClass(seededString("list0")), + TestStringValueClass(seededString("list1")) + ), + inlineIntList = + listOf(TestIntValueClass(seededInt("list0")), TestIntValueClass(seededInt("list1"))), + intList = listOf(seededInt("list0"), seededInt("list1")), + longList = listOf(seededLong("list0"), seededLong("list1")), + shortList = listOf(seededShort("list0"), seededShort("list1")), + stringList = listOf(seededString("list0"), seededString("list1")), + testDataList = + listOf( + TestData2.newInstance(seededString("list0")), + TestData2.newInstance(seededString("list1")) + ), + booleanNull = null, + byteNull = null, + charNull = null, + doubleNull = null, + enumNull = null, + floatNull = null, + inlineStringNull = null, + inlineIntNull = null, + intNull = null, + longNull = null, + shortNull = null, + stringNull = null, + testDataNull = null, + booleanNullable = seededBoolean("nullable"), + byteNullable = seededByte("nullable"), + charNullable = seededChar("nullable"), + doubleNullable = seededDouble("nullable"), + enumNullable = seededEnum("nullable"), + floatNullable = seededFloat("nullable"), + inlineStringNullable = TestStringValueClass(seededString("nullable")), + inlineIntNullable = TestIntValueClass(seededInt("nullable")), + intNullable = seededInt("nullable"), + longNullable = seededLong("nullable"), + shortNullable = seededShort("nullable"), + stringNullable = seededString("nullable"), + testDataNullable = TestData2.newInstance(seededString("nullable")), + booleanNullableList = listOf(seededBoolean("nlist0"), seededBoolean("nlist1"), null), + byteNullableList = listOf(seededByte("nlist0"), seededByte("nlist1"), null), + charNullableList = listOf(seededChar("nlist0"), seededChar("nlist1"), null), + doubleNullableList = listOf(seededDouble("nlist0"), seededDouble("nlist1"), null), + enumNullableList = listOf(seededEnum("nlist0"), seededEnum("nlist1"), null), + floatNullableList = listOf(seededFloat("nlist0"), seededFloat("nlist1"), null), + inlineStringNullableList = + listOf( + TestStringValueClass(seededString("nlist0")), + TestStringValueClass(seededString("nlist1")), + null + ), + inlineIntNullableList = + listOf( + TestIntValueClass(seededInt("nlist0")), + TestIntValueClass(seededInt("nlist1")), + null + ), + intNullableList = listOf(seededInt("nlist0"), seededInt("nlist1"), null), + longNullableList = listOf(seededLong("nlist0"), seededLong("nlist1"), null), + shortNullableList = listOf(seededShort("nlist0"), seededShort("nlist1"), null), + stringNullableList = listOf(seededString("nlist0"), seededString("nlist1"), null), + testDataNullableList = + listOf( + TestData2.newInstance(seededString("nlist0")), + TestData2.newInstance(seededString("nlist1")) + ), + nested = if (nesting <= 0) null else newInstance("${seed}nest${nesting}", nesting - 1), + unit = Unit, + nullUnit = null, + nullableUnit = Unit, + listOfUnit = listOf(Unit, Unit), + listOfNullableUnit = listOf(Unit, null, Unit, null), + ) + } + } + } +} + +/** + * Creates and returns a new instance with the exact same property values but with all lists of + * [Unit] to be empty. This may be useful if testing an encoder/decoder that does not support + * [kotlinx.serialization.descriptors.StructureKind.OBJECT] in lists. + */ +fun SerializationTestData.AllTheTypes.withEmptyUnitLists(): SerializationTestData.AllTheTypes = + copy( + listOfUnit = emptyList(), + listOfNullableUnit = emptyList(), + nested = nested?.withEmptyUnitLists() + ) + +private fun String.seededBoolean(id: String): Boolean = seededInt(id) % 2 == 0 + +private fun String.seededByte(id: String): Byte = seededInt(id).toByte() + +private fun String.seededChar(id: String): Char = get(abs(id.hashCode()) % length) + +private fun String.seededDouble(id: String): Double = seededLong(id).toDouble() / PI + +private fun String.seededEnum(id: String): SerializationTestData.TestEnum = + SerializationTestData.TestEnum.values().let { it[abs(seededInt(id)) % it.size] } + +private fun String.seededFloat(id: String): Float = (seededInt(id).toFloat() / PI.toFloat()) + +private fun String.seededInt(id: String): Int = (hashCode() * id.hashCode()) + +private fun String.seededLong(id: String): Long = (hashCode().toLong() * id.hashCode().toLong()) + +private fun String.seededShort(id: String): Short = seededInt(id).toShort() + +private fun String.seededString(id: String): String = "${this}_${id}" diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/Sha512EncoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/Sha512EncoderTest.kt new file mode 100644 index 00000000000..639ddd58275 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/Sha512EncoderTest.kt @@ -0,0 +1,164 @@ +// Copyright 2023 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. + +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class) + +package com.google.firebase.dataconnect + +import com.google.common.truth.Truth.assertThat +import kotlinx.serialization.ExperimentalSerializationApi +import org.junit.Test + +class Sha512EncoderTest { + + @Test + fun `calculateSha512() should return the same value for the same object`() { + val obj = SerializationTestData.AllTheTypes.newInstance() + assertThat(calculateSha512(obj)).isEqualTo(calculateSha512(obj)) + } + + @Test + fun `calculateSha512() should return the same value for distinct, but equal, objects`() { + val obj1 = SerializationTestData.AllTheTypes.newInstance(seed = "foo") + val obj2 = SerializationTestData.AllTheTypes.newInstance(seed = "foo") + assertThat(calculateSha512(obj1)).isEqualTo(calculateSha512(obj2)) + } + + @Test + fun `calculateSha512() should return different values for lists with same elements in different order`() { + val obj1 = listOf(1, 2, 3) + val obj2 = listOf(1, 3, 2) + + val result1 = calculateSha512(obj1) + val result2 = calculateSha512(obj2) + + assertThat(result1).isNotEqualTo(result2) + } + + @Test + fun `calculateSha512() should return different values for different objects`() { + val obj1 = SerializationTestData.AllTheTypes.newInstance(seed = "foo") + val obj2 = SerializationTestData.AllTheTypes.newInstance(seed = "bar") + assertThat(calculateSha512(obj1)).isNotEqualTo(calculateSha512(obj2)) + } + + @Test + fun `calculateSha512() should return different values for different Byte values`() { + assertThat(calculateSha512(42.toByte())).isNotEqualTo(calculateSha512(43.toByte())) + } + + @Test + fun `calculateSha512() should return different values for different Char values`() { + assertThat(calculateSha512('a')).isNotEqualTo(calculateSha512('b')) + } + + @Test + fun `calculateSha512() should return different values for different Double values`() { + assertThat(calculateSha512(42.0)).isNotEqualTo(calculateSha512(43.0)) + } + + @Test + fun `calculateSha512() should return different values for different Enum values`() { + assertThat(calculateSha512(SerializationTestData.TestEnum.A)) + .isNotEqualTo(calculateSha512(SerializationTestData.TestEnum.B)) + } + + @Test + fun `calculateSha512() should return different values for different Float values`() { + assertThat(calculateSha512(42.0.toFloat())).isNotEqualTo(calculateSha512(43.0.toFloat())) + } + + @Test + fun `calculateSha512() should return different values for different Inline String values`() { + assertThat(SerializationTestData.TestStringValueClass("foo")) + .isNotEqualTo(calculateSha512(SerializationTestData.TestStringValueClass("bar"))) + } + + @Test + fun `calculateSha512() should return different values for different Inline Int values`() { + assertThat(SerializationTestData.TestIntValueClass(42)) + .isNotEqualTo(calculateSha512(SerializationTestData.TestIntValueClass(43))) + } + + @Test + fun `calculateSha512() should return different values for different Int values`() { + assertThat(calculateSha512(42)).isNotEqualTo(calculateSha512(43)) + } + + @Test + fun `calculateSha512() should return different values for different Long values`() { + assertThat(calculateSha512(42.toLong())).isNotEqualTo(calculateSha512(43.toLong())) + } + + @Test + fun `calculateSha512() should return different values for different null values`() { + assertThat(calculateSha512(null)).isNotEqualTo(calculateSha512("foo")) + } + + @Test + fun `calculateSha512() should return the same value for null`() { + assertThat(calculateSha512(null)).isEqualTo(calculateSha512(null)) + } + + @Test + fun `calculateSha512() should return different values for different Short values`() { + assertThat(calculateSha512(42.toShort())).isNotEqualTo(calculateSha512(43.toShort())) + } + + @Test + fun `calculateSha512() should return different values for different String values`() { + assertThat(calculateSha512("foo")).isNotEqualTo(calculateSha512("bar")) + } + + @Test + fun `calculateSha512() should return same value for list of Unit of same length`() { + val list = listOf(Unit, Unit, Unit) + assertThat(calculateSha512(list)).isEqualTo(calculateSha512(list)) + } + + @Test + fun `calculateSha512() should return same value for list of nullable Unit of same length, ending with null`() { + val list = listOf(Unit, Unit, null) + assertThat(calculateSha512(list)).isEqualTo(calculateSha512(list)) + } + + @Test + fun `calculateSha512() should return same value for list of nullable Unit of same length, ending with Unit`() { + val list = listOf(Unit, null, null) + assertThat(calculateSha512(list)).isEqualTo(calculateSha512(list)) + } + + @Test + fun `calculateSha512() should return different value for list of Unit of different length`() { + val list1 = listOf(Unit, Unit, Unit) + val list2 = listOf(Unit, Unit) + assertThat(calculateSha512(list1)).isNotEqualTo(calculateSha512(list2)) + } + + @Test + fun `calculateSha512() should return different for list of nullable Unit of different length`() { + val list1 = listOf(Unit, Unit) + val list2 = listOf(Unit, null, Unit) + assertThat(calculateSha512(list1)).isNotEqualTo(calculateSha512(list2)) + } + + @Test + fun `calculateSha512() should return different for list of nullable Unit of same length`() { + val list1 = listOf(Unit, Unit, null) + val list2 = listOf(Unit, null, Unit) + assertThat(calculateSha512(list1)).isNotEqualTo(calculateSha512(list2)) + } + + // TODO: add tests for nullables, lists, maps, classes, and objects +} From e923e52ee7a708504d9d5818b7db697022acd00a Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 24 Nov 2023 16:21:35 -0500 Subject: [PATCH 074/573] QueryManager.kt added with absolutely minimal functionality, and wired in. (#517) --- .../dataconnect/DataConnectGrpcClient.kt | 22 +- .../dataconnect/FirebaseDataConnect.kt | 18 +- .../firebase/dataconnect/QueryManager.kt | 67 ++++ .../firebase/dataconnect/Sha512Encoder.kt | 374 ------------------ .../firebase/dataconnect/Sha512FromStruct.kt | 60 +++ .../com/google/firebase/dataconnect/Util.kt | 25 ++ .../firebase/dataconnect/Sha512EncoderTest.kt | 164 -------- .../google/firebase/dataconnect/UtilTest.kt | 89 +++++ 8 files changed, 259 insertions(+), 560 deletions(-) create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt delete mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512Encoder.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512FromStruct.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt delete mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/Sha512EncoderTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/UtilTest.kt diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 228147d54a4..abc52a40d1a 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -88,26 +88,22 @@ internal class DataConnectGrpcClient( private val grpcStub: DataServiceCoroutineStub by lazy { DataServiceCoroutineStub(grpcChannel) } - suspend fun executeQuery( - operationName: String, - variables: VariablesType, - variablesSerializer: SerializationStrategy, - dataDeserializer: DeserializationStrategy - ): DataConnectResult { + data class OperationResult(val data: Struct?, val errors: List) + + suspend fun executeQuery(operationName: String, variables: Struct): OperationResult { val request = executeQueryRequest { this.name = requestName this.operationName = operationName - this.variables = encodeToStruct(variablesSerializer, variables) + this.variables = variables } logger.debug { "executeQuery() sending request: $request" } val response = grpcStub.executeQuery(request) logger.debug { "executeQuery() got response: $response" } - return DataConnectResult( - variables = variables, - data = response.data.decode(dataDeserializer), - errors = response.errorsList.map { it.decode() } + return OperationResult( + data = if (response.hasData()) response.data else null, + errors = response.errorsList.map { it.toDataConnectError() } ) } @@ -130,7 +126,7 @@ internal class DataConnectGrpcClient( return DataConnectResult( variables = variables, data = response.data.decode(dataDeserializer), - errors = response.errorsList.map { it.decode() } + errors = response.errorsList.map { it.toDataConnectError() } ) } @@ -153,5 +149,5 @@ fun ListValue.decodePath() = } } -fun GraphqlError.decode() = +fun GraphqlError.toDataConnectError() = DataConnectError(message = message, path = path.decodePath(), extensions = emptyMap()) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index f06e0f1f56b..b8fe69173ab 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -88,19 +88,19 @@ internal constructor( .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } } + // This reference MUST only be set or dereferenced from code running on `sequentialDispatcher`. + private val queryManager: QueryManager by lazy { + if (closed) { + throw IllegalStateException("instance has been closed") + } + QueryManager(grpcClient) + } + internal suspend fun executeQuery( ref: QueryRef, variables: V ): DataConnectResult = - withContext(sequentialDispatcher) { grpcClient } - .run { - executeQuery( - operationName = ref.operationName, - variables = variables, - variablesSerializer = ref.variablesSerializer, - dataDeserializer = ref.dataDeserializer - ) - } + withContext(sequentialDispatcher) { queryManager }.execute(ref, variables) internal suspend fun executeMutation( ref: MutationRef, diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt new file mode 100644 index 00000000000..07377871cf2 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -0,0 +1,67 @@ +// Copyright 2023 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.dataconnect + +import com.google.protobuf.Struct +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +internal class QueryManager(private val grpcClient: DataConnectGrpcClient) { + + private val mutex = Mutex() + private val queryStateByQuery = mutableMapOf() + + suspend fun execute(ref: QueryRef, variables: V): DataConnectResult { + val variablesStruct = encodeToStruct(ref.variablesSerializer, variables) + val variablesSha512 = calculateSha512(variablesStruct).toHexString() + val queryStateKey = + QueryStateKey(operationName = ref.operationName, variablesSha512 = variablesSha512) + + val queryState = + mutex.withLock { + queryStateByQuery.getOrPut(queryStateKey) { + QueryState( + grpcClient = grpcClient, + operationName = ref.operationName, + variables = variablesStruct + ) + } + } + + val operationResult = queryState.execute() + if (operationResult.data === null) { + // TODO: include the variables and error list in the thrown exception + throw DataConnectException("no data included in result") + } + + return DataConnectResult( + variables = variables, + data = decodeFromStruct(ref.dataDeserializer, operationResult.data), + errors = operationResult.errors, + ) + } +} + +private data class QueryStateKey(val operationName: String, val variablesSha512: String) + +private class QueryState( + private val grpcClient: DataConnectGrpcClient, + private val operationName: String, + private val variables: Struct +) { + + suspend fun execute(): DataConnectGrpcClient.OperationResult { + return grpcClient.executeQuery(operationName = operationName, variables = variables) + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512Encoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512Encoder.kt deleted file mode 100644 index 781321469be..00000000000 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512Encoder.kt +++ /dev/null @@ -1,374 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) - -package com.google.firebase.dataconnect - -import java.io.DataOutputStream -import java.io.OutputStream -import java.security.DigestOutputStream -import java.security.MessageDigest -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.CompositeEncoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.modules.EmptySerializersModule -import kotlinx.serialization.serializer - -inline fun calculateSha512(value: T) = calculateSha512(serializer(), value) - -fun calculateSha512(serializer: SerializationStrategy, value: T): ByteArray = - Sha512Encoder().apply { encodeSerializableValue(serializer, value) }.digest.digest() - -private class Sha512Encoder : Encoder { - - val digest = newSha512MessageDigest() - val out = DataOutputStream(DigestOutputStream(NullOutputStream, digest)) - - override val serializersModule = EmptySerializersModule() - - override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { - return when (val kind = descriptor.kind) { - is StructureKind.LIST -> { - out.writeTag(Tag.ListBegin) - Sha512ListEncoder(this, endTag = Tag.ListEnd) - } - is StructureKind.MAP -> { - out.writeTag(Tag.MapBegin) - Sha512MapEncoder(this, endTag = Tag.MapEnd) - } - is StructureKind.CLASS -> { - out.writeTag(Tag.ClassBegin) - Sha512MapEncoder(this, endTag = Tag.ClassEnd) - } - is StructureKind.OBJECT -> { - out.writeTag(Tag.ObjectBegin) - Sha512ObjectEncoder(this, endTag = Tag.ObjectEnd) - } - else -> throw SerializationException("beginStructure unexpected SerialDescriptor.kind: $kind") - } - } - - override fun encodeBoolean(value: Boolean) { - out.writeTag(Tag.Boolean) - out.writeBoolean(value) - } - - override fun encodeByte(value: Byte) { - out.writeTag(Tag.Byte) - out.writeByte(value.toInt()) - } - - override fun encodeChar(value: Char) { - out.writeTag(Tag.Char) - out.writeChar(value.code) - } - - override fun encodeDouble(value: Double) { - out.writeTag(Tag.Double) - out.writeDouble(value) - } - - override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { - out.writeTag(Tag.Enum) - out.writeInt(index) - } - - override fun encodeFloat(value: Float) { - out.writeTag(Tag.Float) - out.writeFloat(value) - } - - override fun encodeInline(descriptor: SerialDescriptor): Encoder { - out.writeTag(Tag.Inline) - return this - } - - override fun encodeInt(value: Int) { - out.writeTag(Tag.Int) - out.writeInt(value) - } - - override fun encodeLong(value: Long) { - out.writeTag(Tag.Long) - out.writeLong(value) - } - - @ExperimentalSerializationApi - override fun encodeNull() { - out.writeTag(Tag.Null) - } - - override fun encodeShort(value: Short) { - out.writeTag(Tag.Short) - out.writeShort(value.toInt()) - } - - override fun encodeString(value: String) { - out.writeTag(Tag.String) - out.writeUTF(value) - } -} - -private class Sha512ListEncoder(val parentEncoder: Sha512Encoder, val endTag: Tag) : - CompositeEncoder { - - override val serializersModule = EmptySerializersModule() - - private val elements = mutableMapOf() - - override fun endStructure(descriptor: SerialDescriptor) { - parentEncoder.out.run { - val digestBytes = ByteArray(parentEncoder.digest.digestLength) - var expectedNextIndex = 0 - elements.entries - .sortedBy { (index, _) -> index } - .forEach { (index, encoder) -> - if (index != expectedNextIndex) { - throw SerializationException("got index $index, but expected $expectedNextIndex") - } - expectedNextIndex++ - writeInt(index) - encoder.digest.digest(digestBytes, 0, digestBytes.size) - write(digestBytes) - } - writeTag(endTag) - } - } - - override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { - elements[index] = Sha512Encoder().apply { encodeBoolean(value) } - } - - override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { - elements[index] = Sha512Encoder().apply { encodeByte(value) } - } - - override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { - elements[index] = Sha512Encoder().apply { encodeChar(value) } - } - - override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { - elements[index] = Sha512Encoder().apply { encodeDouble(value) } - } - - override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { - elements[index] = Sha512Encoder().apply { encodeFloat(value) } - } - - override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { - return Sha512Encoder().also { elements[index] = it } - } - - override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { - elements[index] = Sha512Encoder().apply { encodeInt(value) } - } - - override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { - elements[index] = Sha512Encoder().apply { encodeLong(value) } - } - - override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { - elements[index] = Sha512Encoder().apply { encodeShort(value) } - } - - override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { - elements[index] = Sha512Encoder().apply { encodeString(value) } - } - - override fun encodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T - ) { - elements[index] = Sha512Encoder().apply { encodeSerializableValue(serializer, value) } - } - - @ExperimentalSerializationApi - override fun encodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T? - ) { - elements[index] = Sha512Encoder().apply { encodeNullableSerializableValue(serializer, value) } - } -} - -private class Sha512MapEncoder(val parentEncoder: Sha512Encoder, val endTag: Tag) : - CompositeEncoder { - - override val serializersModule = EmptySerializersModule() - - private val elements = mutableMapOf() - - override fun endStructure(descriptor: SerialDescriptor) { - parentEncoder.out.run { - val digestBytes = ByteArray(parentEncoder.digest.digestLength) - elements.entries - .sortedBy { (key, _) -> key } - .forEach { (key, encoder) -> - writeUTF(key) - encoder.digest.digest(digestBytes, 0, digestBytes.size) - write(digestBytes) - } - writeTag(endTag) - } - } - - override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { - elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeBoolean(value) } - } - - override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { - elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeByte(value) } - } - - override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { - elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeChar(value) } - } - - override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { - elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeDouble(value) } - } - - override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { - elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeFloat(value) } - } - - override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { - return Sha512Encoder().also { elements[descriptor.getElementName(index)] = it } - } - - override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { - elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeInt(value) } - } - - override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { - elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeLong(value) } - } - - override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { - elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeShort(value) } - } - - override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { - elements[descriptor.getElementName(index)] = Sha512Encoder().apply { encodeString(value) } - } - - override fun encodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T - ) { - elements[descriptor.getElementName(index)] = - Sha512Encoder().apply { encodeSerializableValue(serializer, value) } - } - - @ExperimentalSerializationApi - override fun encodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T? - ) { - elements[descriptor.getElementName(index)] = - Sha512Encoder().apply { encodeNullableSerializableValue(serializer, value) } - } -} - -private class Sha512ObjectEncoder(val parentEncoder: Sha512Encoder, val endTag: Tag) : - CompositeEncoder { - - override val serializersModule = EmptySerializersModule() - - override fun endStructure(descriptor: SerialDescriptor) { - parentEncoder.out.writeTag(endTag) - } - - override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) = - notSupported() - - override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) = - notSupported() - - override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) = - notSupported() - - override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = - notSupported() - - override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) = - notSupported() - - override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder = - notSupported() - - override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) = - notSupported() - - override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) = - notSupported() - - override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) = - notSupported() - - override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) = - notSupported() - - override fun encodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T - ) = notSupported() - - @ExperimentalSerializationApi - override fun encodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T? - ) = notSupported() - - private fun notSupported(): Nothing = - throw UnsupportedOperationException( - "The only valid method call on Sha512ObjectEncoder is endStructure()" - ) -} - -private enum class Tag { - ListBegin, - ListEnd, - MapBegin, - MapEnd, - ClassBegin, - ClassEnd, - ObjectBegin, - ObjectEnd, - Boolean, - Byte, - Char, - Double, - Enum, - Float, - Inline, - Int, - Long, - Null, - Short, - String -} - -private fun DataOutputStream.writeTag(tag: Tag) = writeInt(tag.ordinal) - -private object NullOutputStream : OutputStream() { - override fun write(b: Int) {} - override fun write(b: ByteArray?) {} - override fun write(b: ByteArray?, off: Int, len: Int) {} -} - -private fun newSha512MessageDigest(): MessageDigest = MessageDigest.getInstance("SHA-512") diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512FromStruct.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512FromStruct.kt new file mode 100644 index 00000000000..0a1ed8b307e --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512FromStruct.kt @@ -0,0 +1,60 @@ +// Copyright 2023 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.dataconnect + +import com.google.protobuf.Struct +import com.google.protobuf.Value +import com.google.protobuf.Value.KindCase +import java.io.DataOutputStream +import java.security.DigestOutputStream +import java.security.MessageDigest + +fun calculateSha512(struct: Struct): ByteArray = + calculateSha512(Value.newBuilder().setStructValue(struct).build()) + +fun calculateSha512(value: Value): ByteArray { + val digest = MessageDigest.getInstance("SHA-512") + val out = DataOutputStream(DigestOutputStream(NullOutputStream, digest)) + + val calculateDigest = + DeepRecursiveFunction { + val kind = it.kindCase + out.writeInt(kind.ordinal) + + when (kind) { + KindCase.NULL_VALUE -> { + /* nothing to write for null */ + } + KindCase.BOOL_VALUE -> out.writeBoolean(it.boolValue) + KindCase.NUMBER_VALUE -> out.writeDouble(it.numberValue) + KindCase.STRING_VALUE -> out.writeUTF(it.stringValue) + KindCase.LIST_VALUE -> + it.listValue.valuesList.forEachIndexed { index, elementValue -> + out.writeInt(index) + callRecursive(elementValue) + } + KindCase.STRUCT_VALUE -> + it.structValue.fieldsMap.entries + .sortedBy { (key, _) -> key } + .forEach { (key, elementValue) -> + out.writeUTF(key) + callRecursive(elementValue) + } + else -> throw IllegalArgumentException("unsupported kind: $kind") + } + } + + calculateDigest(value) + return digest.digest() +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt new file mode 100644 index 00000000000..83c232d61ca --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -0,0 +1,25 @@ +// Copyright 2023 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.dataconnect + +import java.io.OutputStream + +fun ByteArray.toHexString(): String = + joinToString(separator = "") { it.toUByte().toInt().toString(16).padStart(2, '0') } + +object NullOutputStream : OutputStream() { + override fun write(b: Int) {} + override fun write(b: ByteArray?) {} + override fun write(b: ByteArray?, off: Int, len: Int) {} +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/Sha512EncoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/Sha512EncoderTest.kt deleted file mode 100644 index 639ddd58275..00000000000 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/Sha512EncoderTest.kt +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2023 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. - -@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class) - -package com.google.firebase.dataconnect - -import com.google.common.truth.Truth.assertThat -import kotlinx.serialization.ExperimentalSerializationApi -import org.junit.Test - -class Sha512EncoderTest { - - @Test - fun `calculateSha512() should return the same value for the same object`() { - val obj = SerializationTestData.AllTheTypes.newInstance() - assertThat(calculateSha512(obj)).isEqualTo(calculateSha512(obj)) - } - - @Test - fun `calculateSha512() should return the same value for distinct, but equal, objects`() { - val obj1 = SerializationTestData.AllTheTypes.newInstance(seed = "foo") - val obj2 = SerializationTestData.AllTheTypes.newInstance(seed = "foo") - assertThat(calculateSha512(obj1)).isEqualTo(calculateSha512(obj2)) - } - - @Test - fun `calculateSha512() should return different values for lists with same elements in different order`() { - val obj1 = listOf(1, 2, 3) - val obj2 = listOf(1, 3, 2) - - val result1 = calculateSha512(obj1) - val result2 = calculateSha512(obj2) - - assertThat(result1).isNotEqualTo(result2) - } - - @Test - fun `calculateSha512() should return different values for different objects`() { - val obj1 = SerializationTestData.AllTheTypes.newInstance(seed = "foo") - val obj2 = SerializationTestData.AllTheTypes.newInstance(seed = "bar") - assertThat(calculateSha512(obj1)).isNotEqualTo(calculateSha512(obj2)) - } - - @Test - fun `calculateSha512() should return different values for different Byte values`() { - assertThat(calculateSha512(42.toByte())).isNotEqualTo(calculateSha512(43.toByte())) - } - - @Test - fun `calculateSha512() should return different values for different Char values`() { - assertThat(calculateSha512('a')).isNotEqualTo(calculateSha512('b')) - } - - @Test - fun `calculateSha512() should return different values for different Double values`() { - assertThat(calculateSha512(42.0)).isNotEqualTo(calculateSha512(43.0)) - } - - @Test - fun `calculateSha512() should return different values for different Enum values`() { - assertThat(calculateSha512(SerializationTestData.TestEnum.A)) - .isNotEqualTo(calculateSha512(SerializationTestData.TestEnum.B)) - } - - @Test - fun `calculateSha512() should return different values for different Float values`() { - assertThat(calculateSha512(42.0.toFloat())).isNotEqualTo(calculateSha512(43.0.toFloat())) - } - - @Test - fun `calculateSha512() should return different values for different Inline String values`() { - assertThat(SerializationTestData.TestStringValueClass("foo")) - .isNotEqualTo(calculateSha512(SerializationTestData.TestStringValueClass("bar"))) - } - - @Test - fun `calculateSha512() should return different values for different Inline Int values`() { - assertThat(SerializationTestData.TestIntValueClass(42)) - .isNotEqualTo(calculateSha512(SerializationTestData.TestIntValueClass(43))) - } - - @Test - fun `calculateSha512() should return different values for different Int values`() { - assertThat(calculateSha512(42)).isNotEqualTo(calculateSha512(43)) - } - - @Test - fun `calculateSha512() should return different values for different Long values`() { - assertThat(calculateSha512(42.toLong())).isNotEqualTo(calculateSha512(43.toLong())) - } - - @Test - fun `calculateSha512() should return different values for different null values`() { - assertThat(calculateSha512(null)).isNotEqualTo(calculateSha512("foo")) - } - - @Test - fun `calculateSha512() should return the same value for null`() { - assertThat(calculateSha512(null)).isEqualTo(calculateSha512(null)) - } - - @Test - fun `calculateSha512() should return different values for different Short values`() { - assertThat(calculateSha512(42.toShort())).isNotEqualTo(calculateSha512(43.toShort())) - } - - @Test - fun `calculateSha512() should return different values for different String values`() { - assertThat(calculateSha512("foo")).isNotEqualTo(calculateSha512("bar")) - } - - @Test - fun `calculateSha512() should return same value for list of Unit of same length`() { - val list = listOf(Unit, Unit, Unit) - assertThat(calculateSha512(list)).isEqualTo(calculateSha512(list)) - } - - @Test - fun `calculateSha512() should return same value for list of nullable Unit of same length, ending with null`() { - val list = listOf(Unit, Unit, null) - assertThat(calculateSha512(list)).isEqualTo(calculateSha512(list)) - } - - @Test - fun `calculateSha512() should return same value for list of nullable Unit of same length, ending with Unit`() { - val list = listOf(Unit, null, null) - assertThat(calculateSha512(list)).isEqualTo(calculateSha512(list)) - } - - @Test - fun `calculateSha512() should return different value for list of Unit of different length`() { - val list1 = listOf(Unit, Unit, Unit) - val list2 = listOf(Unit, Unit) - assertThat(calculateSha512(list1)).isNotEqualTo(calculateSha512(list2)) - } - - @Test - fun `calculateSha512() should return different for list of nullable Unit of different length`() { - val list1 = listOf(Unit, Unit) - val list2 = listOf(Unit, null, Unit) - assertThat(calculateSha512(list1)).isNotEqualTo(calculateSha512(list2)) - } - - @Test - fun `calculateSha512() should return different for list of nullable Unit of same length`() { - val list1 = listOf(Unit, Unit, null) - val list2 = listOf(Unit, null, Unit) - assertThat(calculateSha512(list1)).isNotEqualTo(calculateSha512(list2)) - } - - // TODO: add tests for nullables, lists, maps, classes, and objects -} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/UtilTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/UtilTest.kt new file mode 100644 index 00000000000..6f9013b8334 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/UtilTest.kt @@ -0,0 +1,89 @@ +// Copyright 2023 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.dataconnect + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class UtilTest { + + @Test + fun `ByteArray toHexString() on empty byte array`() { + val emptyByteArray = byteArrayOf() + assertThat(emptyByteArray.toHexString()).isEqualTo("") + } + + @Test + fun `ByteArray toHexString() on byte array with 1 element of value 0`() { + val byteArray = byteArrayOf(0) + assertThat(byteArray.toHexString()).isEqualTo("00") + } + + @Test + fun `ByteArray toHexString() on byte array with 1 element of value 1`() { + val byteArray = byteArrayOf(1) + assertThat(byteArray.toHexString()).isEqualTo("01") + } + + @Test + fun `ByteArray toHexString() on byte array with 1 element of value 0xff`() { + val byteArray = byteArrayOf(0xff.toByte()) + assertThat(byteArray.toHexString()).isEqualTo("ff") + } + + @Test + fun `ByteArray toHexString() on byte array with 1 element of value -1`() { + val byteArray = byteArrayOf(-1) + assertThat(byteArray.toHexString()).isEqualTo("ff") + } + + @Test + fun `ByteArray toHexString() on byte array with 1 element of value MIN_VALUE`() { + val byteArray = byteArrayOf(Byte.MIN_VALUE) + assertThat(byteArray.toHexString()).isEqualTo("80") + } + + @Test + fun `ByteArray toHexString() on byte array with 1 element of value MAX_VALUE`() { + val byteArray = byteArrayOf(Byte.MAX_VALUE) + assertThat(byteArray.toHexString()).isEqualTo("7f") + } + + @Test + fun `ByteArray toHexString() on byte array containing all possible values`() { + val byteArray = + buildList { + for (i in 0 until 512) { + add(i.toByte()) + } + } + .toByteArray() + assertThat(byteArray.toHexString()) + .isEqualTo( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2" + + "b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455" + + "565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8" + + "08182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aa" + + "abacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d" + + "5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292" + + "a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f5051525354" + + "55565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7" + + "f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9" + + "aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d" + + "4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + ) + } +} From c6eec2a21ad604ff8cdc2fef7f21a310847479e1 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Sat, 25 Nov 2023 03:08:17 -0500 Subject: [PATCH 075/573] QueryManager.kt: Queue up calls to execute() so that it scales --- .../dataconnect/FirebaseDataConnect.kt | 2 +- .../firebase/dataconnect/QueryManager.kt | 42 +++++++++++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index b8fe69173ab..87294db6204 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -93,7 +93,7 @@ internal constructor( if (closed) { throw IllegalStateException("instance has been closed") } - QueryManager(grpcClient) + QueryManager(grpcClient, coroutineScope) } internal suspend fun executeQuery( diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 07377871cf2..0500e6ae63d 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -14,10 +14,16 @@ package com.google.firebase.dataconnect import com.google.protobuf.Struct +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -internal class QueryManager(private val grpcClient: DataConnectGrpcClient) { +internal class QueryManager( + private val grpcClient: DataConnectGrpcClient, + private val coroutineScope: CoroutineScope +) { private val mutex = Mutex() private val queryStateByQuery = mutableMapOf() @@ -34,7 +40,8 @@ internal class QueryManager(private val grpcClient: DataConnectGrpcClient) { QueryState( grpcClient = grpcClient, operationName = ref.operationName, - variables = variablesStruct + variables = variablesStruct, + coroutineScope = coroutineScope, ) } } @@ -58,10 +65,37 @@ private data class QueryStateKey(val operationName: String, val variablesSha512: private class QueryState( private val grpcClient: DataConnectGrpcClient, private val operationName: String, - private val variables: Struct + private val variables: Struct, + private val coroutineScope: CoroutineScope, ) { + private val mutex = Mutex() + private var job: Deferred? = null + suspend fun execute(): DataConnectGrpcClient.OperationResult { - return grpcClient.executeQuery(operationName = operationName, variables = variables) + // Wait for the current job to complete (if any), and ignore its result. Waiting avoids running + // multiple queries in parallel, which would not scale. + val originalJob = mutex.withLock { job }?.also { it.join() } + + // Now that the job that was in progress when this method started has completed, we can run our + // own query. But we're racing with other concurrent invocations of this method. The first one + // wins and launches the new job, then awaits its completion; the others simply await + // completion of the new job that was started by the winner. + val newJob = + mutex.withLock { + job.let { currentJob -> + if (currentJob !== null && currentJob !== originalJob) { + currentJob + } else { + coroutineScope + .async { + grpcClient.executeQuery(operationName = operationName, variables = variables) + } + .also { newJob -> job = newJob } + } + } + } + + return newJob.await() } } From 93d46f9ee42c7ac8a4922ca3b5a483ce311e36b6 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 27 Nov 2023 11:22:47 -0500 Subject: [PATCH 076/573] QueryManager.kt: reference count the queries (to avoid unbounded memory consumption) --- .../firebase/dataconnect/QueryManager.kt | 105 +++++++++++++----- .../com/google/firebase/dataconnect/Util.kt | 2 + 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 0500e6ae63d..9ccc3d9e19e 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -16,40 +16,21 @@ package com.google.firebase.dataconnect import com.google.protobuf.Struct import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext -internal class QueryManager( - private val grpcClient: DataConnectGrpcClient, - private val coroutineScope: CoroutineScope -) { - - private val mutex = Mutex() - private val queryStateByQuery = mutableMapOf() +internal class QueryManager(grpcClient: DataConnectGrpcClient, coroutineScope: CoroutineScope) { + private val queryStates = QueryStates(grpcClient, coroutineScope) suspend fun execute(ref: QueryRef, variables: V): DataConnectResult { - val variablesStruct = encodeToStruct(ref.variablesSerializer, variables) - val variablesSha512 = calculateSha512(variablesStruct).toHexString() - val queryStateKey = - QueryStateKey(operationName = ref.operationName, variablesSha512 = variablesSha512) + val operationResult = queryStates.letQueryState(ref, variables) { it.execute() } - val queryState = - mutex.withLock { - queryStateByQuery.getOrPut(queryStateKey) { - QueryState( - grpcClient = grpcClient, - operationName = ref.operationName, - variables = variablesStruct, - coroutineScope = coroutineScope, - ) - } - } - - val operationResult = queryState.execute() if (operationResult.data === null) { // TODO: include the variables and error list in the thrown exception - throw DataConnectException("no data included in result") + throw DataConnectException("no data included in result: errors=${operationResult.errors}") } return DataConnectResult( @@ -63,12 +44,12 @@ internal class QueryManager( private data class QueryStateKey(val operationName: String, val variablesSha512: String) private class QueryState( + val key: QueryStateKey, private val grpcClient: DataConnectGrpcClient, private val operationName: String, private val variables: Struct, private val coroutineScope: CoroutineScope, ) { - private val mutex = Mutex() private var job: Deferred? = null @@ -79,7 +60,7 @@ private class QueryState( // Now that the job that was in progress when this method started has completed, we can run our // own query. But we're racing with other concurrent invocations of this method. The first one - // wins and launches the new job, then awaits its completion; the others simply await + // wins and launches the new job, then awaits its completion; the others simply await // completion of the new job that was started by the winner. val newJob = mutex.withLock { @@ -99,3 +80,73 @@ private class QueryState( return newJob.await() } } + +private class QueryStates( + private val grpcClient: DataConnectGrpcClient, + private val coroutineScope: CoroutineScope +) { + private val mutex = Mutex() + + // NOTE: All accesses to `queryStateByKey` and the `refCount` field of each value MUST be done + // from a coroutine that has locked `mutex`; otherwise, such accesses (both reads and writes) are + // data races and yield undefined behavior. + private val queryStateByKey = mutableMapOf>() + + suspend fun letQueryState( + ref: QueryRef, + variables: V, + block: suspend (QueryState) -> R + ): R { + val queryState = mutex.withLock { acquireQueryState(ref, variables) } + + return try { + block(queryState) + } finally { + mutex.withLock { withContext(NonCancellable) { releaseQueryState(queryState) } } + } + } + + // NOTE: This function MUST be called from a coroutine that has locked `mutex`. + private fun acquireQueryState(ref: QueryRef, variables: V): QueryState { + val variablesStruct = encodeToStruct(ref.variablesSerializer, variables) + val variablesSha512 = calculateSha512(variablesStruct).toHexString() + val key = QueryStateKey(operationName = ref.operationName, variablesSha512 = variablesSha512) + + val queryState = + queryStateByKey.getOrPut(key) { + ReferenceCounted( + QueryState( + key = key, + grpcClient = grpcClient, + operationName = ref.operationName, + variables = variablesStruct, + coroutineScope = coroutineScope, + ), + refCount = 0 + ) + } + + queryState.refCount++ + + return queryState.obj + } + + // NOTE: This function MUST be called from a coroutine that has locked `mutex`. + private fun releaseQueryState(queryState: QueryState) { + val referenceCountedQueryState = + queryStateByKey[queryState.key].let { + if (it === null) { + error("unexpected null QueryState for key: ${queryState.key}") + } else if (it.obj !== queryState) { + error("unexpected QueryState for key: ${queryState.key}: $it") + } else { + it + } + } + + referenceCountedQueryState.refCount-- + if (referenceCountedQueryState.refCount == 0) { + queryStateByKey.remove(queryState.key) + } + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 83c232d61ca..5e191e97d93 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -23,3 +23,5 @@ object NullOutputStream : OutputStream() { override fun write(b: ByteArray?) {} override fun write(b: ByteArray?, off: Int, len: Int) {} } + +class ReferenceCounted(val obj: T, var refCount: Int) From 68846495f246d4be98f566a0c37a980f1ed53146 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 27 Nov 2023 11:35:42 -0500 Subject: [PATCH 077/573] QuerySubscriptionTest.kt: minor changes to be more kotlin idiomatic --- .../dataconnect/QuerySubscriptionTest.kt | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt index d33f4f47cb3..9423573e8eb 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt @@ -28,6 +28,7 @@ import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePerso import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.subscribe import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute import java.util.concurrent.Executors +import kotlin.math.max import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.* @@ -174,7 +175,7 @@ class QuerySubscriptionTest { querySubscription.flow.map { it.data }.collect(resultsChannel::send) } - val maxHardwareConcurrency = Math.max(2, Runtime.getRuntime().availableProcessors()) + val maxHardwareConcurrency = max(2, Runtime.getRuntime().availableProcessors()) val multiThreadExecutor = Executors.newFixedThreadPool(maxHardwareConcurrency) try { repeat(100000) { multiThreadExecutor.execute(querySubscription::reload) } @@ -199,15 +200,13 @@ private fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = isEqualTo(PersonSchema.GetPersonQuery.Data.Person(name = name, age = age)) private suspend fun ReceiveChannel.purge(timeout: Duration): List = coroutineScope { - mutableListOf() - .also { - while (true) { - try { - withTimeout(timeout) { it.add(receive()) } - } catch (e: TimeoutCancellationException) { - break - } + buildList { + while (true) { + try { + withTimeout(timeout) { add(receive()) } + } catch (e: TimeoutCancellationException) { + break } } - .toList() + } } From 1d66616422e938f35fc8af1f615f6e6760b050a0 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 27 Nov 2023 12:26:22 -0500 Subject: [PATCH 078/573] QueryManager.kt: add extension function DataConnectGrpcClient.OperationResult.toDataConnectResult --- .../firebase/dataconnect/QueryManager.kt | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 9ccc3d9e19e..103fbca320c 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -25,20 +25,9 @@ import kotlinx.coroutines.withContext internal class QueryManager(grpcClient: DataConnectGrpcClient, coroutineScope: CoroutineScope) { private val queryStates = QueryStates(grpcClient, coroutineScope) - suspend fun execute(ref: QueryRef, variables: V): DataConnectResult { - val operationResult = queryStates.letQueryState(ref, variables) { it.execute() } + suspend fun execute(ref: QueryRef, variables: V): DataConnectResult = + queryStates.letQueryState(ref, variables) { it.execute() }.toDataConnectResult(ref, variables) - if (operationResult.data === null) { - // TODO: include the variables and error list in the thrown exception - throw DataConnectException("no data included in result: errors=${operationResult.errors}") - } - - return DataConnectResult( - variables = variables, - data = decodeFromStruct(ref.dataDeserializer, operationResult.data), - errors = operationResult.errors, - ) - } } private data class QueryStateKey(val operationName: String, val variablesSha512: String) @@ -150,3 +139,19 @@ private class QueryStates( } } } + +private fun DataConnectGrpcClient.OperationResult.toDataConnectResult( + ref: QueryRef, + variables: V +): DataConnectResult { + if (data === null) { + // TODO: include the variables and error list in the thrown exception + throw DataConnectException("no data included in result: errors=${errors}") + } + + return DataConnectResult( + variables = variables, + data = decodeFromStruct(ref.dataDeserializer, data), + errors = errors, + ) +} From 2463978d224f1b9f28e55c6f9ded19b610b7b103 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 27 Nov 2023 12:27:23 -0500 Subject: [PATCH 079/573] QueryManager.kt: Rename letQueryState -> withQueryState --- .../kotlin/com/google/firebase/dataconnect/QueryManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 103fbca320c..c34e9633b81 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -26,7 +26,7 @@ internal class QueryManager(grpcClient: DataConnectGrpcClient, coroutineScope: C private val queryStates = QueryStates(grpcClient, coroutineScope) suspend fun execute(ref: QueryRef, variables: V): DataConnectResult = - queryStates.letQueryState(ref, variables) { it.execute() }.toDataConnectResult(ref, variables) + queryStates.withQueryState(ref, variables) { it.execute() }.toDataConnectResult(ref, variables) } @@ -81,7 +81,7 @@ private class QueryStates( // data races and yield undefined behavior. private val queryStateByKey = mutableMapOf>() - suspend fun letQueryState( + suspend fun withQueryState( ref: QueryRef, variables: V, block: suspend (QueryState) -> R From 519ddc88e23dc3864ded66cb9bb42dab8725228c Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 27 Nov 2023 15:40:19 -0500 Subject: [PATCH 080/573] QueryManager.kt: add collectResults() and collectExceptions() --- .../firebase/dataconnect/QueryManager.kt | 193 +++++++++++++++--- .../firebase/dataconnect/generated/Posts.kt | 38 ++-- 2 files changed, 192 insertions(+), 39 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index c34e9633b81..acc81140cdd 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -14,20 +14,54 @@ package com.google.firebase.dataconnect import com.google.protobuf.Struct +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.atomic.AtomicLong import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import kotlinx.serialization.DeserializationStrategy internal class QueryManager(grpcClient: DataConnectGrpcClient, coroutineScope: CoroutineScope) { private val queryStates = QueryStates(grpcClient, coroutineScope) suspend fun execute(ref: QueryRef, variables: V): DataConnectResult = - queryStates.withQueryState(ref, variables) { it.execute() }.toDataConnectResult(ref, variables) + queryStates + .withQueryState(ref, variables) { it.execute(ref.dataDeserializer) } + .toDataConnectResult(variables) + suspend fun collectResults( + ref: QueryRef, + variables: V, + collector: FlowCollector> + ) { + queryStates.withQueryState(ref, variables) { + it.collectResults(ref.dataDeserializer, collector) { toDataConnectResult(variables) } + } + } + + suspend fun collectExceptions( + ref: QueryRef, + variables: V, + collector: FlowCollector + ) { + queryStates.withQueryState(ref, variables) { + it.collectExceptions(ref.dataDeserializer, collector) + } + } + + private companion object { + fun QueryState.ExecuteResult.toDataConnectResult(variables: V) = + DataConnectResult(variables = variables, data = data, errors = errors) + } } private data class QueryStateKey(val operationName: String, val variablesSha512: String) @@ -40,9 +74,27 @@ private class QueryState( private val coroutineScope: CoroutineScope, ) { private val mutex = Mutex() - private var job: Deferred? = null + private var job: Deferred>? = null - suspend fun execute(): DataConnectGrpcClient.OperationResult { + private val dataDeserializers = CopyOnWriteArrayList>() + + private val operationResultFlow = + MutableSharedFlow>( + replay = 1, + extraBufferCapacity = Int.MAX_VALUE, + onBufferOverflow = BufferOverflow.SUSPEND + ) + + private val exceptionFlow = + MutableSharedFlow>( + replay = 1, + extraBufferCapacity = Int.MAX_VALUE, + onBufferOverflow = BufferOverflow.SUSPEND + ) + + data class ExecuteResult(val data: T, val errors: List) + + suspend fun execute(dataDeserializer: DeserializationStrategy): ExecuteResult { // Wait for the current job to complete (if any), and ignore its result. Waiting avoids running // multiple queries in parallel, which would not scale. val originalJob = mutex.withLock { job }?.also { it.join() } @@ -53,20 +105,127 @@ private class QueryState( // completion of the new job that was started by the winner. val newJob = mutex.withLock { + registerDataDeserializer(dataDeserializer) + job.let { currentJob -> if (currentJob !== null && currentJob !== originalJob) { currentJob } else { - coroutineScope - .async { - grpcClient.executeQuery(operationName = operationName, variables = variables) - } - .also { newJob -> job = newJob } + coroutineScope.async { doExecute() }.also { newJob -> job = newJob } } } } - return newJob.await() + // TODO: As an optimization, avoid calling deserialize() if the data was already deserialized + // by someone else. + return newJob.await().obj.deserialize(dataDeserializer) + } + + private suspend fun doExecute(): Numbered { + val sequenceNumber = nextSequenceNumber.incrementAndGet() + + val executeQueryResult = + kotlin.runCatching { + grpcClient.executeQuery(operationName = operationName, variables = variables) + } + + val resultForDataSerializers = Numbered(executeQueryResult, sequenceNumber) + mutex.withLock { dataDeserializers.iterator() }.forEach { it.update(resultForDataSerializers) } + + return executeQueryResult.fold( + onSuccess = { + val numberedResult = Numbered(it, sequenceNumber) + operationResultFlow.emit(numberedResult) + exceptionFlow.emit(Numbered(null, sequenceNumber)) + numberedResult + }, + onFailure = { + exceptionFlow.emit(Numbered(it, sequenceNumber)) + throw it + } + ) + } + + suspend fun collectResults( + dataDeserializer: DeserializationStrategy, + collector: FlowCollector, + mapResult: ExecuteResult.() -> R + ) = + mutex + .withLock { registerDataDeserializer(dataDeserializer) } + .resultFlow + .map { mapResult(it.obj) } + .collect(collector) + + suspend fun collectExceptions( + dataDeserializer: DeserializationStrategy<*>, + collector: FlowCollector + ) = + mutex + .withLock { registerDataDeserializer(dataDeserializer) } + .exceptionFlow + .map { it.obj } + .collect(collector) + + // NOTE: This function MUST be called by a coroutine that has `mutex` locked; otherwise, a data + // race will occur, resulting in undefined behavior. + @Suppress("UNCHECKED_CAST") + private fun registerDataDeserializer( + dataDeserializer: DeserializationStrategy + ): DeserialzerInfo = + dataDeserializers.firstOrNull { it.deserializer === dataDeserializer } as? DeserialzerInfo + ?: DeserialzerInfo(dataDeserializer).also { dataDeserializers.add(it) } + + private companion object { + val nextSequenceNumber = AtomicLong(0) + + fun DataConnectGrpcClient.OperationResult.deserialize( + dataDeserializer: DeserializationStrategy + ): ExecuteResult { + if (data === null) { + // TODO: include the variables and error list in the thrown exception + throw DataConnectException("no data included in result: errors=${errors}") + } + return ExecuteResult(data = decodeFromStruct(dataDeserializer, data), errors = errors) + } + } + + private data class Numbered(val obj: T, val sequenceNumber: Long) + + private class DeserialzerInfo(val deserializer: DeserializationStrategy) { + private val _resultFlow = + MutableSharedFlow>>( + replay = 1, + extraBufferCapacity = Int.MAX_VALUE, + onBufferOverflow = BufferOverflow.SUSPEND, + ) + + val resultFlow = _resultFlow.asSharedFlow() + + val _exceptionFlow = + MutableSharedFlow>( + replay = 1, + extraBufferCapacity = Int.MAX_VALUE, + onBufferOverflow = BufferOverflow.SUSPEND, + ) + + val exceptionFlow = _exceptionFlow.asSharedFlow() + + suspend fun update(result: Numbered>) { + result.obj.fold( + onSuccess = { + val deserializeResult = kotlin.runCatching { it.deserialize(deserializer) } + deserializeResult.fold( + onSuccess = { + _resultFlow.emit(Numbered(it, result.sequenceNumber)) + _exceptionFlow.emit(Numbered(null, result.sequenceNumber)) + }, + onFailure = { _exceptionFlow.emit(Numbered(it, result.sequenceNumber)) } + ) + }, + onFailure = { _exceptionFlow.emit(Numbered(it, result.sequenceNumber)) }, + ) + } } } @@ -139,19 +298,3 @@ private class QueryStates( } } } - -private fun DataConnectGrpcClient.OperationResult.toDataConnectResult( - ref: QueryRef, - variables: V -): DataConnectResult { - if (data === null) { - // TODO: include the variables and error list in the thrown exception - throw DataConnectException("no data included in result: errors=${errors}") - } - - return DataConnectResult( - variables = variables, - data = decodeFromStruct(ref.dataDeserializer, data), - errors = errors, - ) -} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt index 68605a7934b..67acc83c15f 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt @@ -41,19 +41,29 @@ class PostsOperationSet( settings ) - val createPost: MutationRef - get() = - dataConnect.mutation( - operationName = "createPost", - variablesSerializer = serializer(), - dataDeserializer = serializer() - ) + // Use `lazy` to ensure that there is only one instance of the [QueryRef] so that the serializer + // instances encapsulated therein are also singletons. This ensures that query caching works as + // expected, since the cache key of query results includes the serializer references, compared + // using referential equality. If [serializer()] was documented to guarantee that it always + // returns the same instance, then this singleton-ness would not be necessary. + val createPost: MutationRef by lazy { + dataConnect.mutation( + operationName = "createPost", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + } - val getPost: QueryRef - get() = - dataConnect.query( - operationName = "getPost", - variablesSerializer = serializer(), - dataDeserializer = serializer() - ) + // Use `lazy` to ensure that there is only one instance of the [QueryRef] so that the serializer + // instances encapsulated therein are also singletons. This ensures that query caching works as + // expected, since the cache key of query results includes the serializer references, compared + // using referential equality. If [serializer()] was documented to guarantee that it always + // returns the same instance, then this singleton-ness would not be necessary. + val getPost: QueryRef by lazy { + dataConnect.query( + operationName = "getPost", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + } } From 30df2f595fa6b08c98c141daa318eec0655df172 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 27 Nov 2023 15:43:31 -0500 Subject: [PATCH 081/573] QueryManager.kt: remove Numbered --- .../firebase/dataconnect/QueryManager.kt | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index acc81140cdd..3743cb4c661 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -15,7 +15,6 @@ package com.google.firebase.dataconnect import com.google.protobuf.Struct import java.util.concurrent.CopyOnWriteArrayList -import java.util.concurrent.atomic.AtomicLong import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.NonCancellable @@ -74,19 +73,19 @@ private class QueryState( private val coroutineScope: CoroutineScope, ) { private val mutex = Mutex() - private var job: Deferred>? = null + private var job: Deferred? = null private val dataDeserializers = CopyOnWriteArrayList>() private val operationResultFlow = - MutableSharedFlow>( + MutableSharedFlow( replay = 1, extraBufferCapacity = Int.MAX_VALUE, onBufferOverflow = BufferOverflow.SUSPEND ) private val exceptionFlow = - MutableSharedFlow>( + MutableSharedFlow( replay = 1, extraBufferCapacity = Int.MAX_VALUE, onBufferOverflow = BufferOverflow.SUSPEND @@ -118,29 +117,25 @@ private class QueryState( // TODO: As an optimization, avoid calling deserialize() if the data was already deserialized // by someone else. - return newJob.await().obj.deserialize(dataDeserializer) + return newJob.await().deserialize(dataDeserializer) } - private suspend fun doExecute(): Numbered { - val sequenceNumber = nextSequenceNumber.incrementAndGet() - + private suspend fun doExecute(): DataConnectGrpcClient.OperationResult { val executeQueryResult = kotlin.runCatching { grpcClient.executeQuery(operationName = operationName, variables = variables) } - val resultForDataSerializers = Numbered(executeQueryResult, sequenceNumber) - mutex.withLock { dataDeserializers.iterator() }.forEach { it.update(resultForDataSerializers) } + mutex.withLock { dataDeserializers.iterator() }.forEach { it.update(executeQueryResult) } return executeQueryResult.fold( onSuccess = { - val numberedResult = Numbered(it, sequenceNumber) - operationResultFlow.emit(numberedResult) - exceptionFlow.emit(Numbered(null, sequenceNumber)) - numberedResult + operationResultFlow.emit(it) + exceptionFlow.emit(null) + it }, onFailure = { - exceptionFlow.emit(Numbered(it, sequenceNumber)) + exceptionFlow.emit(it) throw it } ) @@ -154,7 +149,7 @@ private class QueryState( mutex .withLock { registerDataDeserializer(dataDeserializer) } .resultFlow - .map { mapResult(it.obj) } + .map { mapResult(it) } .collect(collector) suspend fun collectExceptions( @@ -164,7 +159,7 @@ private class QueryState( mutex .withLock { registerDataDeserializer(dataDeserializer) } .exceptionFlow - .map { it.obj } + .map { it } .collect(collector) // NOTE: This function MUST be called by a coroutine that has `mutex` locked; otherwise, a data @@ -177,8 +172,6 @@ private class QueryState( ?: DeserialzerInfo(dataDeserializer).also { dataDeserializers.add(it) } private companion object { - val nextSequenceNumber = AtomicLong(0) - fun DataConnectGrpcClient.OperationResult.deserialize( dataDeserializer: DeserializationStrategy ): ExecuteResult { @@ -190,11 +183,9 @@ private class QueryState( } } - private data class Numbered(val obj: T, val sequenceNumber: Long) - private class DeserialzerInfo(val deserializer: DeserializationStrategy) { private val _resultFlow = - MutableSharedFlow>>( + MutableSharedFlow>( replay = 1, extraBufferCapacity = Int.MAX_VALUE, onBufferOverflow = BufferOverflow.SUSPEND, @@ -203,7 +194,7 @@ private class QueryState( val resultFlow = _resultFlow.asSharedFlow() val _exceptionFlow = - MutableSharedFlow>( + MutableSharedFlow( replay = 1, extraBufferCapacity = Int.MAX_VALUE, onBufferOverflow = BufferOverflow.SUSPEND, @@ -211,19 +202,19 @@ private class QueryState( val exceptionFlow = _exceptionFlow.asSharedFlow() - suspend fun update(result: Numbered>) { - result.obj.fold( + suspend fun update(result: Result) { + result.fold( onSuccess = { val deserializeResult = kotlin.runCatching { it.deserialize(deserializer) } deserializeResult.fold( onSuccess = { - _resultFlow.emit(Numbered(it, result.sequenceNumber)) - _exceptionFlow.emit(Numbered(null, result.sequenceNumber)) + _resultFlow.emit(it) + _exceptionFlow.emit(null) }, - onFailure = { _exceptionFlow.emit(Numbered(it, result.sequenceNumber)) } + onFailure = { _exceptionFlow.emit(it) } ) }, - onFailure = { _exceptionFlow.emit(Numbered(it, result.sequenceNumber)) }, + onFailure = { _exceptionFlow.emit(it) }, ) } } From 47b9028b04af7f9e7d379ae65a29d32572c11ecb Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 27 Nov 2023 21:02:02 -0500 Subject: [PATCH 082/573] Rename androidTest test files to end with "IntegrationTest" instead of just "Test" --- ...DataConnectTest.kt => FirebaseDataConnectIntegrationTest.kt} | 2 +- ...ySubscriptionTest.kt => QuerySubscriptionIntegrationTest.kt} | 2 +- .../generated/{PostsTest.kt => PostsIntegrationTest.kt} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/{FirebaseDataConnectTest.kt => FirebaseDataConnectIntegrationTest.kt} (99%) rename firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/{QuerySubscriptionTest.kt => QuerySubscriptionIntegrationTest.kt} (99%) rename firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/{PostsTest.kt => PostsIntegrationTest.kt} (99%) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt similarity index 99% rename from firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt index 6582ce97dcb..ecf073738a3 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt @@ -35,7 +35,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class FirebaseDataConnectTest { +class FirebaseDataConnectIntegrationTest { @get:Rule val firebaseAppFactory = TestFirebaseAppFactory() @get:Rule val dataConnectFactory = TestDataConnectFactory() diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt similarity index 99% rename from firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt index 9423573e8eb..4869495ca99 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt @@ -40,7 +40,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class QuerySubscriptionTest { +class QuerySubscriptionIntegrationTest { @get:Rule val dataConnectFactory = TestDataConnectFactory() @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt similarity index 99% rename from firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt rename to firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt index ca716cb8d8e..50d848a6f87 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt @@ -39,7 +39,7 @@ import org.junit.runner.RunWith @OptIn(FlowPreview::class) @RunWith(AndroidJUnit4::class) -class PostsTest { +class PostsIntegrationTest { @get:Rule val dataConnectFactory = TestDataConnectFactory() @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() From 38fda7105a8e73a16a80ac9bfb6dc454ee9cd01c Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 27 Nov 2023 22:17:00 -0500 Subject: [PATCH 083/573] QueryRefIntegrationTest.kt added, with some basic test coverage --- .../testing_graphql_schemas/alltypes/ops.gql | 83 +++++++++ .../alltypes/schema.gql | 26 +++ .../dataconnect/QueryRefIntegrationTest.kt | 160 ++++++++++++++++++ .../testutil/schemas/AllTypesSchema.kt | 130 ++++++++++++++ .../testutil/schemas/PersonSchema.kt | 3 +- 5 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql create mode 100644 firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql new file mode 100644 index 00000000000..bfa558d8f7a --- /dev/null +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql @@ -0,0 +1,83 @@ +# Copyright 2023 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. + +mutation createPrimitive($data: Primitive_Data! @pick(fields: ["id", "idFieldNullable", "intField", "intFieldNullable", "floatField", "floatFieldNullable", "booleanField", "booleanFieldNullable", "stringField", "stringFieldNullable"])) @auth(is: PUBLIC) { + primitive_insert(data: $data) +} + +mutation deletePrimitive($id: ID!) @auth(is: PUBLIC) { + primitive_delete(id: $id) +} + +mutation updateIntField($id: ID!, $data: Primitive_Data! @pick(fields: ["intField"])) @auth(is: PUBLIC) { + primitive_update(id: $id, data: $data) +} + +mutation updateIntFieldNullable($id: ID!, $data: Primitive_Data! @pick(fields: ["intFieldNullable"])) @auth(is: PUBLIC) { + primitive_update(id: $id, data: $data) +} + +mutation updateFloatField($id: ID!, $data: Primitive_Data! @pick(fields: ["floatField"])) @auth(is: PUBLIC) { + primitive_update(id: $id, data: $data) +} + +mutation updateFloatFieldNullable($id: ID!, $data: Primitive_Data! @pick(fields: ["floatFieldNullable"])) @auth(is: PUBLIC) { + primitive_update(id: $id, data: $data) +} + +mutation updateBooleanField($id: ID!, $data: Primitive_Data! @pick(fields: ["booleanField"])) @auth(is: PUBLIC) { + primitive_update(id: $id, data: $data) +} + +mutation updateBooleanFieldNullable($id: ID!, $data: Primitive_Data! @pick(fields: ["booleanFieldNullable"])) @auth(is: PUBLIC) { + primitive_update(id: $id, data: $data) +} + +mutation updateStringField($id: ID!, $data: Primitive_Data! @pick(fields: ["stringField"])) @auth(is: PUBLIC) { + primitive_update(id: $id, data: $data) +} + +mutation updateStringFieldNullable($id: ID!, $data: Primitive_Data! @pick(fields: ["stringFieldNullable"])) @auth(is: PUBLIC) { + primitive_update(id: $id, data: $data) +} + +query getPrimitive($id: ID!) @auth(is: PUBLIC) { + primitive(id: $id) { + id + idFieldNullable + intField + intFieldNullable + floatField + floatFieldNullable + booleanField + booleanFieldNullable + stringField + stringFieldNullable + } +} + +query getAllPrimitives @auth(is: PUBLIC) { + primitives { + id + idFieldNullable + intField + intFieldNullable + floatField + floatFieldNullable + booleanField + booleanFieldNullable + stringField + stringFieldNullable + } +} diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql new file mode 100644 index 00000000000..53f8a85a920 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql @@ -0,0 +1,26 @@ +# Copyright 2023 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. + +type Primitive @table { + id: ID! + idFieldNullable: ID + intField: Int! + intFieldNullable: Int + floatField: Float! + floatFieldNullable: Float + booleanField: Boolean! + booleanFieldNullable: Boolean + stringField: String! + stringFieldNullable: String +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt new file mode 100644 index 00000000000..bcdf91fb148 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -0,0 +1,160 @@ +// Copyright 2023 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. + +@file:OptIn(FlowPreview::class) + +package com.google.firebase.dataconnect + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule +import com.google.firebase.dataconnect.testutil.TestDataConnectFactory +import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema +import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.Companion.newAllTypesSchema +import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.CreatePrimitiveMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.GetPrimitiveQuery.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.Companion.newPersonSchema +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetAllPeopleQuery.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class QueryRefIntegrationTest { + + @get:Rule val dataConnectFactory = TestDataConnectFactory() + @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + + private val personSchema: PersonSchema by lazy { + dataConnectFactory.newPersonSchema().also { runBlocking { it.installEmulatorSchema() } } + } + + private val allTypesSchema: AllTypesSchema by lazy { + dataConnectFactory.newAllTypesSchema().also { runBlocking { it.installEmulatorSchema() } } + } + + @Test + fun executeWithASingleResultReturnsTheCorrectResult(): Unit = runBlocking { + personSchema.createPerson.execute(id = "TestId1", name = "TestName1", age = 42) + personSchema.createPerson.execute(id = "TestId2", name = "TestName2", age = 43) + personSchema.createPerson.execute(id = "TestId3", name = "TestName3", age = 44) + + val result = personSchema.getPerson.execute(id = "TestId2") + + assertThat(result.data.person?.name).isEqualTo("TestName2") + assertThat(result.data.person?.age).isEqualTo(43) + assertThat(result.errors).isEmpty() + } + + @Test + fun executeWithASingleResultReturnsTheUpdatedResult(): Unit = runBlocking { + personSchema.createPerson.execute(id = "TestId", name = "TestName", age = 42) + personSchema.updatePerson.execute(id = "TestId", name = "NewTestName", age = 99) + + val result = personSchema.getPerson.execute(id = "TestId") + + assertThat(result.data.person?.name).isEqualTo("NewTestName") + assertThat(result.data.person?.age).isEqualTo(99) + assertThat(result.errors).isEmpty() + } + + @Test + fun executeWithASingleResultReturnsNullIfNotFound(): Unit = runBlocking { + personSchema.createPerson.execute(id = "TestId", name = "TestName", age = 42) + + val result = personSchema.getPerson.execute(id = "NotTheTestId") + + assertThat(result.data.person).isNull() + assertThat(result.errors).isEmpty() + } + + @Test + fun executeWithAListResultReturnsAllResults(): Unit = runBlocking { + personSchema.createPerson.execute(id = "TestId1", name = "TestName1", age = 42) + personSchema.createPerson.execute(id = "TestId2", name = "TestName2", age = 43) + personSchema.createPerson.execute(id = "TestId3", name = "TestName3", age = 44) + + val result = personSchema.getAllPeople.execute() + + assertThat(result.data.people) + .containsExactly( + PersonSchema.GetAllPeopleQuery.Data.Person(id = "TestId1", name = "TestName1", age = 42), + PersonSchema.GetAllPeopleQuery.Data.Person(id = "TestId2", name = "TestName2", age = 43), + PersonSchema.GetAllPeopleQuery.Data.Person(id = "TestId3", name = "TestName3", age = 44), + ) + assertThat(result.errors).isEmpty() + } + + @Test + fun executeWithAllPrimitiveGraphQLTypesInDataNoneNull(): Unit = runBlocking { + allTypesSchema.createPrimitive.execute( + id = "TestId", + idFieldNullable = "TestNullableId", + intField = 42, + intFieldNullable = 43, + floatField = 123.45f, + floatFieldNullable = 678.91f, + booleanField = true, + booleanFieldNullable = false, + stringField = "TestString", + stringFieldNullable = "TestNullableString", + ) + + val result = allTypesSchema.getPrimitive.execute(id = "TestId") + + val primitive = result.data.primitive ?: error("result.data was null, but expected non") + assertThat(primitive.id).isEqualTo("TestId") + assertThat(primitive.idFieldNullable).isEqualTo("TestNullableId") + assertThat(primitive.intField).isEqualTo(42) + assertThat(primitive.intFieldNullable).isEqualTo(43) + assertThat(primitive.floatField).isEqualTo(123.45f) + assertThat(primitive.floatFieldNullable).isEqualTo(678.91f) + assertThat(primitive.booleanField).isEqualTo(true) + assertThat(primitive.booleanFieldNullable).isEqualTo(false) + assertThat(primitive.stringField).isEqualTo("TestString") + assertThat(primitive.stringFieldNullable).isEqualTo("TestNullableString") + } + + @Test + fun executeWithAllPrimitiveGraphQLTypesInDataNullablesAreNull(): Unit = runBlocking { + allTypesSchema.createPrimitive.execute( + id = "TestId", + idFieldNullable = null, + intField = 42, + intFieldNullable = null, + floatField = 123.45f, + floatFieldNullable = null, + booleanField = true, + booleanFieldNullable = null, + stringField = "TestString", + stringFieldNullable = null, + ) + + val result = allTypesSchema.getPrimitive.execute(id = "TestId") + + val primitive = result.data.primitive ?: error("result.data was null, but expected non") + assertThat(primitive.idFieldNullable).isNull() + assertThat(primitive.intFieldNullable).isNull() + assertThat(primitive.floatFieldNullable).isNull() + assertThat(primitive.booleanFieldNullable).isNull() + assertThat(primitive.stringFieldNullable).isNull() + } +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt new file mode 100644 index 00000000000..cd85885b838 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt @@ -0,0 +1,130 @@ +// Copyright 2023 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.dataconnect.testutil.schemas + +import com.google.firebase.dataconnect.FirebaseDataConnect +import com.google.firebase.dataconnect.MutationRef +import com.google.firebase.dataconnect.QueryRef +import com.google.firebase.dataconnect.mutation +import com.google.firebase.dataconnect.query +import com.google.firebase.dataconnect.testutil.TestDataConnectFactory +import com.google.firebase.dataconnect.testutil.installEmulatorSchema +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer + +class AllTypesSchema(private val dataConnect: FirebaseDataConnect) { + + init { + dataConnect.serviceConfig.operationSet.let { + require(it == OPERATION_SET) { + "The given FirebaseDataConnect has operationSet=$it, but expected $OPERATION_SET" + } + } + } + + suspend fun installEmulatorSchema() { + dataConnect.installEmulatorSchema("testing_graphql_schemas/alltypes") + } + + object CreatePrimitiveMutation { + @Serializable + data class PrimitiveData( + val id: String, + val idFieldNullable: String?, + val intField: Int, + val intFieldNullable: Int?, + val floatField: Float, + val floatFieldNullable: Float?, + val booleanField: Boolean, + val booleanFieldNullable: Boolean?, + val stringField: String, + val stringFieldNullable: String?, + ) + @Serializable data class Variables(val data: PrimitiveData) + + suspend fun MutationRef.execute( + id: String, + idFieldNullable: String?, + intField: Int, + intFieldNullable: Int?, + floatField: Float, + floatFieldNullable: Float?, + booleanField: Boolean, + booleanFieldNullable: Boolean?, + stringField: String, + stringFieldNullable: String?, + ) = + execute( + Variables( + PrimitiveData( + id = id, + idFieldNullable = idFieldNullable, + intField = intField, + intFieldNullable = intFieldNullable, + floatField = floatField, + floatFieldNullable = floatFieldNullable, + booleanField = booleanField, + booleanFieldNullable = booleanFieldNullable, + stringField = stringField, + stringFieldNullable = stringFieldNullable, + ) + ) + ) + } + + val createPrimitive = + dataConnect.mutation( + operationName = "createPrimitive", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + + object GetPrimitiveQuery { + @Serializable data class Variables(val id: String) + + @Serializable + data class Data(val primitive: PrimitiveData?) { + @Serializable + data class PrimitiveData( + val id: String, + val idFieldNullable: String?, + val intField: Int, + val intFieldNullable: Int?, + val floatField: Float, + val floatFieldNullable: Float?, + val booleanField: Boolean, + val booleanFieldNullable: Boolean?, + val stringField: String, + val stringFieldNullable: String?, + ) + } + + suspend fun QueryRef.execute(id: String) = execute(Variables(id = id)) + } + + val getPrimitive = + dataConnect.query( + operationName = "getPrimitive", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + + companion object { + const val OPERATION_SET = "ops" + + fun TestDataConnectFactory.newAllTypesSchema() = + AllTypesSchema(newInstance(operationSet = OPERATION_SET)) + } +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index e85c8753bb4..68514668d61 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -85,8 +85,7 @@ class PersonSchema(private val dataConnect: FirebaseDataConnect) { ) object GetPersonQuery { - @Serializable - data class Variables(val id: String, val name: String? = null, val age: Int? = null) + @Serializable data class Variables(val id: String) @Serializable data class Data(val person: Person?) { From 27bda7a127062a562c75df50d8861d630329ba68 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 28 Nov 2023 12:12:51 -0500 Subject: [PATCH 084/573] various testing improvements --- .../FirebaseDataConnectIntegrationTest.kt | 2 +- .../dataconnect/QueryRefIntegrationTest.kt | 38 ++-- .../QuerySubscriptionIntegrationTest.kt | 208 ++++++++---------- .../generated/PostsIntegrationTest.kt | 102 ++++----- .../testutil/TestDataConnectFactory.kt | 8 + .../testutil/schemas/AllTypesSchema.kt | 4 +- .../testutil/schemas/PersonSchema.kt | 4 +- .../testutil/schemas/PersonSchemaTest.kt | 63 +++--- .../firebase/dataconnect/DataConnectResult.kt | 2 +- .../FirebaseDataConnectSettings.kt | 4 +- .../dataconnect/DataConnectResultTest.kt | 8 + .../FirebaseDataConnectSettingsTest.kt | 7 + .../dataconnect/testutil/TestUtils.kt | 29 +++ .../testutil/testutil.gradle.kts | 1 + 14 files changed, 242 insertions(+), 238 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt index ecf073738a3..34e859ae6a6 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt @@ -37,9 +37,9 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class FirebaseDataConnectIntegrationTest { + @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() @get:Rule val firebaseAppFactory = TestFirebaseAppFactory() @get:Rule val dataConnectFactory = TestDataConnectFactory() - @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() @Test fun getInstance_without_specifying_an_app_should_use_the_default_app() { diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt index bcdf91fb148..2afd5b13e96 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -20,19 +20,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory -import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema -import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.Companion.newAllTypesSchema import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.CreatePrimitiveMutation.execute import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.GetPrimitiveQuery.execute -import com.google.firebase.dataconnect.testutil.schemas.PersonSchema -import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.Companion.newPersonSchema import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetAllPeopleQuery.Data.Person import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetAllPeopleQuery.execute import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.execute import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.* import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -40,19 +37,16 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QueryRefIntegrationTest { - @get:Rule val dataConnectFactory = TestDataConnectFactory() @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val dataConnectFactory = TestDataConnectFactory() - private val personSchema: PersonSchema by lazy { - dataConnectFactory.newPersonSchema().also { runBlocking { it.installEmulatorSchema() } } - } - - private val allTypesSchema: AllTypesSchema by lazy { - dataConnectFactory.newAllTypesSchema().also { runBlocking { it.installEmulatorSchema() } } - } + private val personSchema + get() = dataConnectFactory.personSchema + private val allTypesSchema + get() = dataConnectFactory.allTypesSchema @Test - fun executeWithASingleResultReturnsTheCorrectResult(): Unit = runBlocking { + fun executeWithASingleResultReturnsTheCorrectResult() = runTest { personSchema.createPerson.execute(id = "TestId1", name = "TestName1", age = 42) personSchema.createPerson.execute(id = "TestId2", name = "TestName2", age = 43) personSchema.createPerson.execute(id = "TestId3", name = "TestName3", age = 44) @@ -65,7 +59,7 @@ class QueryRefIntegrationTest { } @Test - fun executeWithASingleResultReturnsTheUpdatedResult(): Unit = runBlocking { + fun executeWithASingleResultReturnsTheUpdatedResult() = runTest { personSchema.createPerson.execute(id = "TestId", name = "TestName", age = 42) personSchema.updatePerson.execute(id = "TestId", name = "NewTestName", age = 99) @@ -77,7 +71,7 @@ class QueryRefIntegrationTest { } @Test - fun executeWithASingleResultReturnsNullIfNotFound(): Unit = runBlocking { + fun executeWithASingleResultReturnsNullIfNotFound() = runTest { personSchema.createPerson.execute(id = "TestId", name = "TestName", age = 42) val result = personSchema.getPerson.execute(id = "NotTheTestId") @@ -87,7 +81,7 @@ class QueryRefIntegrationTest { } @Test - fun executeWithAListResultReturnsAllResults(): Unit = runBlocking { + fun executeWithAListResultReturnsAllResults() = runTest { personSchema.createPerson.execute(id = "TestId1", name = "TestName1", age = 42) personSchema.createPerson.execute(id = "TestId2", name = "TestName2", age = 43) personSchema.createPerson.execute(id = "TestId3", name = "TestName3", age = 44) @@ -96,15 +90,15 @@ class QueryRefIntegrationTest { assertThat(result.data.people) .containsExactly( - PersonSchema.GetAllPeopleQuery.Data.Person(id = "TestId1", name = "TestName1", age = 42), - PersonSchema.GetAllPeopleQuery.Data.Person(id = "TestId2", name = "TestName2", age = 43), - PersonSchema.GetAllPeopleQuery.Data.Person(id = "TestId3", name = "TestName3", age = 44), + Person(id = "TestId1", name = "TestName1", age = 42), + Person(id = "TestId2", name = "TestName2", age = 43), + Person(id = "TestId3", name = "TestName3", age = 44), ) assertThat(result.errors).isEmpty() } @Test - fun executeWithAllPrimitiveGraphQLTypesInDataNoneNull(): Unit = runBlocking { + fun executeWithAllPrimitiveGraphQLTypesInDataNoneNull() = runTest { allTypesSchema.createPrimitive.execute( id = "TestId", idFieldNullable = "TestNullableId", @@ -134,7 +128,7 @@ class QueryRefIntegrationTest { } @Test - fun executeWithAllPrimitiveGraphQLTypesInDataNullablesAreNull(): Unit = runBlocking { + fun executeWithAllPrimitiveGraphQLTypesInDataNullablesAreNull() = runTest { allTypesSchema.createPrimitive.execute( id = "TestId", idFieldNullable = null, diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt index 4869495ca99..9a6c2e2e633 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -@file:OptIn(FlowPreview::class) +@file:OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) package com.google.firebase.dataconnect @@ -22,19 +22,21 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory -import com.google.firebase.dataconnect.testutil.schemas.PersonSchema -import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.Companion.newPersonSchema +import com.google.firebase.dataconnect.testutil.delayIgnoringTestScheduler +import com.google.firebase.dataconnect.testutil.delayUntil import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.Data as GetPersonQueryData +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.Variables as GetPersonQueryVariables import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.subscribe import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.Executors import kotlin.math.max -import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* -import org.junit.Before +import kotlinx.coroutines.test.* import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -42,16 +44,11 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QuerySubscriptionIntegrationTest { - @get:Rule val dataConnectFactory = TestDataConnectFactory() @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val dataConnectFactory = TestDataConnectFactory() - private lateinit var schema: PersonSchema - - @Before - fun initializePersonSchema() { - schema = dataConnectFactory.newPersonSchema() - runBlocking { schema.installEmulatorSchema() } - } + private val schema + get() = dataConnectFactory.personSchema @Test fun lastResult_should_be_null_on_new_instance() { @@ -60,153 +57,126 @@ class QuerySubscriptionIntegrationTest { } @Test - fun lastResult_should_be_equal_to_the_last_collected_result(): Unit = runBlocking { + fun lastResult_should_be_equal_to_the_last_collected_result() = runTest { schema.createPerson.execute(id = "TestId", name = "TestPerson", age = 42) val querySubscription = schema.getPerson.subscribe(id = "42") - val result = querySubscription.flow.timeout(2000.seconds).first() + val result = querySubscription.flow.first() assertThat(querySubscription.lastResult).isEqualTo(result) } @Test - fun reload_should_notify_collecting_flows(): Unit = runBlocking { + fun reload_should_notify_collecting_flows() = runTest { schema.createPerson.execute(id = "TestId12345", name = "Name0", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - withTimeout(2.seconds) { - val resultsChannel = - Channel< - DataConnectResult - >( - capacity = Channel.UNLIMITED - ) - val collectJob = launch { querySubscription.flow.collect(resultsChannel::send) } - - val result1 = resultsChannel.receive() - assertThat(result1.data.person).isEqualToGetPersonQueryResult(name = "Name0", age = 10000) + val resultsChannel = + Channel>( + capacity = Channel.UNLIMITED + ) + backgroundScope.launch { querySubscription.flow.collect(resultsChannel::send) } - schema.updatePerson.execute(id = "TestId12345", name = "Name1", age = 10001) + val result1 = resultsChannel.receive() + assertThat(result1.data.person).isEqualToGetPersonQueryResult(name = "Name0", age = 10000) - querySubscription.reload() - val result2 = resultsChannel.receive() - assertThat(result2.data.person).isEqualToGetPersonQueryResult(name = "Name1", age = 10001) + schema.updatePerson.execute(id = "TestId12345", name = "Name1", age = 10001) - collectJob.cancel() - } + querySubscription.reload() + val result2 = resultsChannel.receive() + assertThat(result2.data.person).isEqualToGetPersonQueryResult(name = "Name1", age = 10001) } @Test - fun flow_collect_should_get_immediately_invoked_with_last_result(): Unit = runBlocking { + fun flow_collect_should_get_immediately_invoked_with_last_result() = runTest { schema.createPerson.execute(id = "TestId12345", name = "TestName", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - withTimeout(2.seconds) { - val result1 = querySubscription.flow.first().data.person - assertThat(result1).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) + val result1 = querySubscription.flow.first().data.person + assertThat(result1).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) - schema.updatePerson.execute(id = "TestId12345", name = "TestName2", age = 10002) + schema.updatePerson.execute(id = "TestId12345", name = "TestName2", age = 10002) - val result2 = querySubscription.flow.first().data.person - assertThat(result2).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) - } + val result2 = querySubscription.flow.first().data.person + assertThat(result2).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) } @Test - fun slow_flows_do_not_block_fast_flows(): Unit = runBlocking { + fun slow_flows_do_not_block_fast_flows() = runTest { schema.createPerson.execute(id = "TestId12345", name = "TestName", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - withTimeout(2.seconds) { - val slowFlowJob = launch { - querySubscription.flow.collect { delay(Integer.MAX_VALUE.seconds) } - } + backgroundScope.launch { querySubscription.flow.collect { delay(Integer.MAX_VALUE.seconds) } } - repeat(5) { - assertWithMessage("fast flow retrieval iteration $it") - .that(querySubscription.flow.first().data.person) - .isEqualToGetPersonQueryResult(name = "TestName", age = 10000) - } - - slowFlowJob.cancel() + repeat(5) { + assertWithMessage("fast flow retrieval iteration $it") + .that(querySubscription.flow.first().data.person) + .isEqualToGetPersonQueryResult(name = "TestName", age = 10000) } } @Test - fun reload_delivers_result_to_all_registered_flows(): Unit = runBlocking { + fun reload_delivers_result_to_all_registered_flows() = runTest { schema.createPerson.execute(id = "TestId12345", name = "TestName0", age = 10000) - val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - - withTimeout(2.seconds) { - val resultsChannel1 = Channel(capacity = Channel.UNLIMITED) - val flowJob1 = launch { - querySubscription.flow.map { it.data }.collect(resultsChannel1::send) - } - val resultsChannel2 = Channel(capacity = Channel.UNLIMITED) - val flowJob2 = launch { - querySubscription.flow.map { it.data }.collect(resultsChannel2::send) - } - resultsChannel1.purge(0.25.seconds).forEach { - assertThat(it.person).isEqualToGetPersonQueryResult(name = "TestName0", age = 10000) - } - resultsChannel2.purge(0.25.seconds).forEach { - assertThat(it.person).isEqualToGetPersonQueryResult(name = "TestName0", age = 10000) - } - - schema.updatePerson.execute(id = "TestId12345", name = "TestName1", age = 10001) - querySubscription.reload() - - assertThat(resultsChannel1.receive().person) - .isEqualToGetPersonQueryResult(name = "TestName1", age = 10001) - assertThat(resultsChannel2.receive().person) - .isEqualToGetPersonQueryResult(name = "TestName1", age = 10001) - - flowJob1.cancel() - flowJob2.cancel() - } + val queryVariables = GetPersonQueryVariables(id = "TestId12345") + val querySubscription = schema.getPerson.subscribe(queryVariables) + val results1 = CopyOnWriteArrayList>() + val results2 = CopyOnWriteArrayList>() + backgroundScope.launch { querySubscription.flow.toList(results1) } + delayUntil("results1.isNotEmpty()") { results1.isNotEmpty() } + backgroundScope.launch { querySubscription.flow.toList(results2) } + delayUntil("results2.isNotEmpty()") { results2.isNotEmpty() } + + schema.updatePerson.execute(id = "TestId12345", name = "TestName9", age = 99999) + querySubscription.reload() + + delayIgnoringTestScheduler(2.seconds) + + val expectedResult1 = + DataConnectResult( + variables = queryVariables, + data = GetPersonQueryData(GetPersonQueryData.Person(name = "TestName0", age = 10000)), + errors = emptyList() + ) + val expectedResult2 = + DataConnectResult( + variables = queryVariables, + data = GetPersonQueryData(GetPersonQueryData.Person(name = "TestName9", age = 99999)), + errors = emptyList() + ) + assertWithMessage("results1") + .that(results1) + .containsExactly(expectedResult1, expectedResult1, expectedResult2) + .inOrder() + assertWithMessage("results2") + .that(results2) + .containsExactly(expectedResult1, expectedResult1, expectedResult2) + .inOrder() } @Test - fun reload_concurrent_invocations_get_conflated() = runBlocking { + fun reload_concurrent_invocations_get_conflated() = runTest { schema.createPerson.execute(id = "TestId12345", name = "Name", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - withTimeout(5.seconds) { - val resultsChannel = Channel(capacity = Channel.UNLIMITED) - val collectJob = launch { - querySubscription.flow.map { it.data }.collect(resultsChannel::send) - } - - val maxHardwareConcurrency = max(2, Runtime.getRuntime().availableProcessors()) - val multiThreadExecutor = Executors.newFixedThreadPool(maxHardwareConcurrency) - try { - repeat(100000) { multiThreadExecutor.execute(querySubscription::reload) } - } finally { - multiThreadExecutor.shutdown() - } - - var resultCount = 0 - while (true) { - resultCount++ - val result = withTimeoutOrNull(1.seconds) { resultsChannel.receive() } ?: break - assertThat(result.person).isEqualToGetPersonQueryResult(name = "Name", age = 10000) - } - assertThat(resultCount).isGreaterThan(0) - - collectJob.cancel() - } - } -} + val resultsChannel = Channel(capacity = Channel.UNLIMITED) + backgroundScope.launch { querySubscription.flow.map { it.data }.collect(resultsChannel::send) } -private fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = - isEqualTo(PersonSchema.GetPersonQuery.Data.Person(name = name, age = age)) + val maxHardwareConcurrency = max(2, Runtime.getRuntime().availableProcessors()) + val multiThreadExecutor = Executors.newFixedThreadPool(maxHardwareConcurrency) + try { + repeat(100000) { multiThreadExecutor.execute(querySubscription::reload) } + } finally { + multiThreadExecutor.shutdown() + } -private suspend fun ReceiveChannel.purge(timeout: Duration): List = coroutineScope { - buildList { + var resultCount = 0 while (true) { - try { - withTimeout(timeout) { add(receive()) } - } catch (e: TimeoutCancellationException) { - break - } + resultCount++ + val result = withTimeoutOrNull(1.seconds) { resultsChannel.receive() } ?: break + assertThat(result.person).isEqualToGetPersonQueryResult(name = "Name", age = 10000) } + assertThat(resultCount).isGreaterThan(0) } } + +private fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = + isEqualTo(GetPersonQueryData.Person(name = name, age = age)) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt index 50d848a6f87..9a67ad92568 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt @@ -22,103 +22,85 @@ import com.google.firebase.dataconnect.FirebaseDataConnectSettings import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory import com.google.firebase.dataconnect.testutil.nextAlphanumericString -import kotlin.math.absoluteValue import kotlin.random.Random -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.timeout -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.runBlocking -import org.junit.Before +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.* import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -@OptIn(FlowPreview::class) @RunWith(AndroidJUnit4::class) class PostsIntegrationTest { - @get:Rule val dataConnectFactory = TestDataConnectFactory() @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val dataConnectFactory = TestDataConnectFactory() - private lateinit var posts: PostsOperationSet - - @Before - fun setUpPostsOperationSet() { - posts = - PostsOperationSet( + private val posts: PostsOperationSet by lazy { + PostsOperationSet( app = Firebase.app, serviceId = "local", location = Random.nextAlphanumericString(), settings = FirebaseDataConnectSettings.emulator ) - dataConnectFactory.adoptInstance(posts.dataConnect) + .also { dataConnectFactory.adoptInstance(it.dataConnect) } } @Test - fun getPostWithNonExistingId() { - runBlocking { - val queryResponse = posts.getPost.execute(id = Random.nextAlphanumericString()) - assertWithMessage("queryResponse").that(queryResponse.data.post).isNull() - } + fun getPostWithNonExistingId() = runTest { + val queryResponse = posts.getPost.execute(id = Random.nextAlphanumericString()) + assertWithMessage("queryResponse").that(queryResponse.data.post).isNull() } @Test - fun createPostThenGetPost() { + fun createPostThenGetPost() = runTest { val postId = Random.nextAlphanumericString() val postContent = Random.Default.nextLong().toString(30) - runBlocking { - posts.createPost.execute(id = postId, content = postContent) + posts.createPost.execute(id = postId, content = postContent) - val queryResponse = posts.getPost.execute(id = postId) - assertWithMessage("queryResponse") - .that(queryResponse.data.post) - .isEqualTo(GetPostQuery.Data.Post(content = postContent, comments = emptyList())) - } + val queryResponse = posts.getPost.execute(id = postId) + assertWithMessage("queryResponse") + .that(queryResponse.data.post) + .isEqualTo(GetPostQuery.Data.Post(content = postContent, comments = emptyList())) } @Test - fun subscribe() { + fun subscribe() = runTest { val postId1 = Random.nextAlphanumericString() - val postContent1 = Random.Default.nextLong().absoluteValue.toString(30) + val postContent1 = Random.nextAlphanumericString() val postId2 = Random.nextAlphanumericString() - val postContent2 = Random.Default.nextLong().absoluteValue.toString(30) + val postContent2 = Random.nextAlphanumericString() - runBlocking { - posts.createPost.execute(id = postId1, content = postContent1) - posts.createPost.execute(id = postId2, content = postContent2) + posts.createPost.execute(id = postId1, content = postContent1) + posts.createPost.execute(id = postId2, content = postContent2) - val querySubscription = posts.getPost.subscribe(id = postId1) - assertWithMessage("lastResult 0").that(querySubscription.lastResult).isNull() + val querySubscription = posts.getPost.subscribe(id = postId1) + assertWithMessage("lastResult 0").that(querySubscription.lastResult).isNull() - val result1 = querySubscription.flow.timeout(5.seconds).first() - assertWithMessage("result1.isSuccess").that(result1.errors).isEmpty() - assertWithMessage("result1.post.content") - .that(result1.data.post?.content) - .isEqualTo(postContent1) + val result1 = querySubscription.flow.first() + assertWithMessage("result1.isSuccess").that(result1.errors).isEmpty() + assertWithMessage("result1.post.content") + .that(result1.data.post?.content) + .isEqualTo(postContent1) - assertWithMessage("lastResult 1").that(querySubscription.lastResult).isEqualTo(result1) + assertWithMessage("lastResult 1").that(querySubscription.lastResult).isEqualTo(result1) - val flow2Job = async { querySubscription.flow.timeout(5.seconds).take(2).toList() } + val flow2Job = backgroundScope.async { querySubscription.flow.take(2).toList() } - querySubscription.update { id = postId2 } + querySubscription.update { id = postId2 } - val results2 = flow2Job.await() - assertWithMessage("results2.size").that(results2.size).isEqualTo(2) - assertWithMessage("results2[0].isSuccess").that(results2[0].errors).isEmpty() - assertWithMessage("results2[1].isSuccess").that(results2[1].errors).isEmpty() - assertWithMessage("results2[0].post.content") - .that(results2[0].data.post?.content) - .isEqualTo(postContent1) - assertWithMessage("results2[1].post.content") - .that(results2[1].data.post?.content) - .isEqualTo(postContent2) + val results2 = flow2Job.await() + assertWithMessage("results2.size").that(results2.size).isEqualTo(2) + assertWithMessage("results2[0].isSuccess").that(results2[0].errors).isEmpty() + assertWithMessage("results2[1].isSuccess").that(results2[1].errors).isEmpty() + assertWithMessage("results2[0].post.content") + .that(results2[0].data.post?.content) + .isEqualTo(postContent1) + assertWithMessage("results2[1].post.content") + .that(results2[1].data.post?.content) + .isEqualTo(postContent2) - assertWithMessage("lastResult 2").that(querySubscription.lastResult).isEqualTo(results2[1]) - } + assertWithMessage("lastResult 2").that(querySubscription.lastResult).isEqualTo(results2[1]) } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt index 94c7dd49921..9759283ecf0 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt @@ -16,7 +16,12 @@ package com.google.firebase.dataconnect.testutil import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.FirebaseDataConnectSettings +import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema +import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.Companion.installAllTypesSchema +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.Companion.installPersonSchema import kotlin.random.Random +import kotlinx.coroutines.runBlocking /** * A JUnit test rule that creates instances of [FirebaseDataConnect] for use during testing, and @@ -25,6 +30,9 @@ import kotlin.random.Random class TestDataConnectFactory : FactoryTestRule() { + val personSchema: PersonSchema by lazy { runBlocking { installPersonSchema() } } + val allTypesSchema: AllTypesSchema by lazy { runBlocking { installAllTypesSchema() } } + fun newInstance( serviceId: String? = null, location: String? = null, diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt index cd85885b838..8f23ca53a2a 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt @@ -124,7 +124,7 @@ class AllTypesSchema(private val dataConnect: FirebaseDataConnect) { companion object { const val OPERATION_SET = "ops" - fun TestDataConnectFactory.newAllTypesSchema() = - AllTypesSchema(newInstance(operationSet = OPERATION_SET)) + suspend fun TestDataConnectFactory.installAllTypesSchema(): AllTypesSchema = + AllTypesSchema(newInstance(operationSet = OPERATION_SET)).apply { installEmulatorSchema() } } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index 68514668d61..4e4f2801903 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -125,7 +125,7 @@ class PersonSchema(private val dataConnect: FirebaseDataConnect) { companion object { const val OPERATION_SET = "ops" - fun TestDataConnectFactory.newPersonSchema() = - PersonSchema(newInstance(operationSet = OPERATION_SET)) + suspend fun TestDataConnectFactory.installPersonSchema(): PersonSchema = + PersonSchema(newInstance(operationSet = OPERATION_SET)).apply { installEmulatorSchema() } } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt index 59a166cd24b..c8bb466bd76 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchemaTest.kt @@ -4,14 +4,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory -import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.Companion.newPersonSchema import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation.execute import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.DeletePersonMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetAllPeopleQuery.Data.Person import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetAllPeopleQuery.execute import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.execute import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute -import kotlinx.coroutines.runBlocking -import org.junit.Before +import kotlinx.coroutines.* +import kotlinx.coroutines.test.* import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -19,29 +19,26 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PersonSchemaTest { - @get:Rule val dataConnectFactory = TestDataConnectFactory() @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val dataConnectFactory = TestDataConnectFactory() - private lateinit var schema: PersonSchema - - @Before - fun initializePersonSchema() { - schema = dataConnectFactory.newPersonSchema() - runBlocking { schema.installEmulatorSchema() } - } + private val schema + get() = dataConnectFactory.personSchema @Test - fun createPersonShouldCreateTheSpecifiedPerson(): Unit = runBlocking { + fun createPersonShouldCreateTheSpecifiedPerson() = runTest { schema.createPerson.execute(id = "1234", name = "TestName", age = 42) val result = schema.getPerson.execute(id = "1234") - assertThat(result.data.person?.name).isEqualTo("TestName") - assertThat(result.data.person?.age).isEqualTo(42) + assertThat(result.data.person).isNotNull() + val person = result.data.person!! + assertThat(person.name).isEqualTo("TestName") + assertThat(person.age).isEqualTo(42) } @Test - fun deletePersonShouldDeleteTheSpecifiedPerson(): Unit = runBlocking { + fun deletePersonShouldDeleteTheSpecifiedPerson() = runTest { schema.createPerson.execute(id = "1234", name = "TestName", age = 42) assertThat(schema.getPerson.execute(id = "1234").data.person).isNotNull() @@ -51,7 +48,7 @@ class PersonSchemaTest { } @Test - fun updatePersonShouldUpdateTheSpecifiedPerson(): Unit = runBlocking { + fun updatePersonShouldUpdateTheSpecifiedPerson() = runTest { schema.createPerson.execute(id = "1234", name = "TestName0", age = 42) schema.updatePerson.execute(id = "1234", name = "TestName99", age = 999) @@ -62,7 +59,7 @@ class PersonSchemaTest { } @Test - fun getPersonShouldReturnThePersonWithTheSpecifiedId(): Unit = runBlocking { + fun getPersonShouldReturnThePersonWithTheSpecifiedId() = runTest { schema.createPerson.execute(id = "111", name = "Name111", age = 111) schema.createPerson.execute(id = "222", name = "Name222", age = 222) schema.createPerson.execute(id = "333", name = "Name333", age = null) @@ -71,16 +68,24 @@ class PersonSchemaTest { val result2 = schema.getPerson.execute(id = "222") val result3 = schema.getPerson.execute(id = "333") - assertThat(result1.data.person?.name).isEqualTo("Name111") - assertThat(result1.data.person?.age).isEqualTo(111) - assertThat(result2.data.person?.name).isEqualTo("Name222") - assertThat(result2.data.person?.age).isEqualTo(222) - assertThat(result3.data.person?.name).isEqualTo("Name333") - assertThat(result3.data.person?.age).isNull() + assertThat(result1.data.person).isNotNull() + val person1 = result1.data.person!! + assertThat(person1.name).isEqualTo("Name111") + assertThat(person1.age).isEqualTo(111) + + assertThat(result2.data.person).isNotNull() + val person2 = result2.data.person!! + assertThat(person2.name).isEqualTo("Name222") + assertThat(person2.age).isEqualTo(222) + + assertThat(result3.data.person).isNotNull() + val person3 = result3.data.person!! + assertThat(person3.name).isEqualTo("Name333") + assertThat(person3.age).isNull() } @Test - fun getPersonShouldReturnNullPersonIfThePersonDoesNotExist(): Unit = runBlocking { + fun getPersonShouldReturnNullPersonIfThePersonDoesNotExist() = runTest { schema.createPerson.execute(id = "111", name = "Name111", age = 111) val result = schema.getPerson.execute(id = "IdOfPersonThatDoesNotExit") @@ -89,12 +94,12 @@ class PersonSchemaTest { } @Test - fun getAllPeopleShouldReturnEmptyListIfTheDatabaseIsEmpty(): Unit = runBlocking { + fun getAllPeopleShouldReturnEmptyListIfTheDatabaseIsEmpty() = runTest { assertThat(schema.getAllPeople.execute().data.people).isEmpty() } @Test - fun getAllPeopleShouldReturnAllPeopleInTheDatabase(): Unit = runBlocking { + fun getAllPeopleShouldReturnAllPeopleInTheDatabase() = runTest { schema.createPerson.execute(id = "111", name = "Name111", age = 111) schema.createPerson.execute(id = "222", name = "Name222", age = 222) schema.createPerson.execute(id = "333", name = "Name333", age = null) @@ -103,9 +108,9 @@ class PersonSchemaTest { assertThat(result.data.people) .containsExactly( - PersonSchema.GetAllPeopleQuery.Data.Person(id = "111", name = "Name111", age = 111), - PersonSchema.GetAllPeopleQuery.Data.Person(id = "222", name = "Name222", age = 222), - PersonSchema.GetAllPeopleQuery.Data.Person(id = "333", name = "Name333", age = null), + Person(id = "111", name = "Name111", age = 111), + Person(id = "222", name = "Name222", age = 222), + Person(id = "333", name = "Name333", age = null) ) } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt index 096409bded8..5c75067c572 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt @@ -32,7 +32,7 @@ private constructor(private val impl: Impl) { override fun hashCode() = impl.hashCode() override fun equals(other: Any?) = (other as? DataConnectResult<*, *>)?.let { it.impl == impl } ?: false - override fun toString() = impl.toString() + override fun toString() = "DataConnectResult(variables=$variables, data=$data, errors=$errors)" private data class Impl( val variables: VariablesType, diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt index eb660d43b22..5c909a3aed1 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettings.kt @@ -50,11 +50,11 @@ class FirebaseDataConnectSettings private constructor(private val values: Settin override fun hashCode() = values.hashCode() override fun toString() = - "FirebaseDataConnectSettings{" + + "FirebaseDataConnectSettings(" + "hostName=$hostName, " + "port=$port, " + "sslEnabled=$sslEnabled" + - "}" + ")" } // Use a data class internally to store the settings to get the convenience of the equals(), diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt index 91c5563ddb7..7fb600d006c 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt @@ -42,6 +42,14 @@ class DataConnectResultTest { assertThat(dataConnectResult.errors).isEmpty() } + @Test + fun `toString() should begin with the class name and contain text in parentheses`() { + val dataConnectResult = + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + assertThat(dataConnectResult.toString()).startsWith("DataConnectResult(") + assertThat(dataConnectResult.toString()).endsWith(")") + } + @Test fun `toString() should incorporate the variables`() { val variables = diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt index 4261aa08d74..8bfb7ee63a1 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt @@ -222,6 +222,13 @@ class FirebaseDataConnectSettingsTest { assertThat(settings1.hashCode()).isNotEqualTo(settings2.hashCode()) } + @Test + fun `toString() should begin with the class name and contain text in parentheses`() { + val toStringResult = FirebaseDataConnectSettings.defaults.toString() + assertThat(toStringResult).startsWith("FirebaseDataConnectSettings(") + assertThat(toStringResult).endsWith(")") + } + @Test fun `toString() should return a string that contains the property values`() { val settings = diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt index e8c65dd6d57..fa5415b004a 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt @@ -5,6 +5,12 @@ import java.util.regex.Pattern import kotlin.math.abs import kotlin.math.min import kotlin.random.Random +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext /** * Asserts that a string contains another string, verifying that the character immediately preceding @@ -40,3 +46,26 @@ fun Random.nextAlphanumericString(length: Int? = null): String = buildString { * the 10 numeric digits. */ fun Long.toAlphaNumericString(): String = toString(36) + +/** + * Calls [kotlinx.coroutines.delay] in such a way that it _really_ will delay, even when called from + * [kotlinx.coroutines.test.runTest], which _skips_ delays. This is achieved by switching contexts + * to a dispatcher that does _not_ use the [kotlinx.coroutines.test.TestCoroutineScheduler] + * scheduler and, therefore, will actually delay, as measured by a wall clock. + */ +suspend fun delayIgnoringTestScheduler(duration: Duration) { + withContext(Dispatchers.Default) { delay(duration) } +} + +/** Delays the current coroutine until the given predicate returns `true`. */ +suspend fun delayUntil(name: String? = null, predicate: () -> Boolean) { + while (!predicate()) { + try { + delayIgnoringTestScheduler(0.2.seconds) + } catch (e: CancellationException) { + throw DelayUntilTimeoutException("delayUntil(name=$name) cancelled") + } + } +} + +class DelayUntilTimeoutException(message: String) : Exception(message) diff --git a/firebase-dataconnect/testutil/testutil.gradle.kts b/firebase-dataconnect/testutil/testutil.gradle.kts index 9eefdb1673b..f2126c9c99f 100644 --- a/firebase-dataconnect/testutil/testutil.gradle.kts +++ b/firebase-dataconnect/testutil/testutil.gradle.kts @@ -36,5 +36,6 @@ android { } dependencies { + api(libs.kotlinx.coroutines.core) api(libs.truth) } From 3164787e8b3255b974605a4d0f569b3524801367 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 28 Nov 2023 14:20:17 -0500 Subject: [PATCH 085/573] executeMutation() fix to throw on error --- .../dataconnect/DataConnectGrpcClient.kt | 39 ++++++++++++------- .../dataconnect/FirebaseDataConnect.kt | 14 +++---- .../firebase/dataconnect/QueryManager.kt | 37 ++++++------------ 3 files changed, 42 insertions(+), 48 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index abc52a40d1a..13733ff70f7 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -28,7 +28,6 @@ import io.grpc.android.AndroidChannelBuilder import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.SerializationStrategy internal class DataConnectGrpcClient( context: Context, @@ -89,6 +88,7 @@ internal class DataConnectGrpcClient( private val grpcStub: DataServiceCoroutineStub by lazy { DataServiceCoroutineStub(grpcChannel) } data class OperationResult(val data: Struct?, val errors: List) + data class DeserialzedOperationResult(val data: T, val errors: List) suspend fun executeQuery(operationName: String, variables: Struct): OperationResult { val request = executeQueryRequest { @@ -107,25 +107,19 @@ internal class DataConnectGrpcClient( ) } - suspend fun executeMutation( - operationName: String, - variables: VariablesType, - variablesSerializer: SerializationStrategy, - dataDeserializer: DeserializationStrategy - ): DataConnectResult { + suspend fun executeMutation(operationName: String, variables: Struct): OperationResult { val request = executeMutationRequest { this.name = requestName this.operationName = operationName - this.variables = encodeToStruct(variablesSerializer, variables) + this.variables = variables } logger.debug { "executeMutation() sending request: $request" } val response = grpcStub.executeMutation(request) logger.debug { "executeMutation() got response: $response" } - return DataConnectResult( - variables = variables, - data = response.data.decode(dataDeserializer), + return OperationResult( + data = if (response.hasData()) response.data else null, errors = response.errorsList.map { it.toDataConnectError() } ) } @@ -137,10 +131,10 @@ internal class DataConnectGrpcClient( } } -fun Struct.decode(deserializer: DeserializationStrategy) = +internal fun Struct.decode(deserializer: DeserializationStrategy) = decodeFromStruct(deserializer, this) -fun ListValue.decodePath() = +internal fun ListValue.decodePath() = valuesList.map { when (val kind = it.kindCase) { Value.KindCase.STRING_VALUE -> DataConnectError.PathSegment.Field(it.stringValue) @@ -149,5 +143,22 @@ fun ListValue.decodePath() = } } -fun GraphqlError.toDataConnectError() = +internal fun GraphqlError.toDataConnectError() = DataConnectError(message = message, path = path.decodePath(), extensions = emptyMap()) + +internal fun DataConnectGrpcClient.OperationResult.deserialize( + dataDeserializer: DeserializationStrategy +): DataConnectGrpcClient.DeserialzedOperationResult { + if (data === null) { + // TODO: include the variables and error list in the thrown exception + throw DataConnectException("no data included in result: errors=${errors}") + } + return DataConnectGrpcClient.DeserialzedOperationResult( + data = decodeFromStruct(dataDeserializer, data), + errors = errors + ) +} + +internal fun DataConnectGrpcClient.DeserialzedOperationResult.toDataConnectResult( + variables: V +) = DataConnectResult(variables = variables, data = data, errors = errors) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 87294db6204..7e3210d2dde 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -107,14 +107,12 @@ internal constructor( variables: V ): DataConnectResult = withContext(sequentialDispatcher) { grpcClient } - .run { - executeMutation( - operationName = ref.operationName, - variables = variables, - variablesSerializer = ref.variablesSerializer, - dataDeserializer = ref.dataDeserializer - ) - } + .executeMutation( + operationName = ref.operationName, + variables = encodeToStruct(ref.variablesSerializer, variables) + ) + .deserialize(ref.dataDeserializer) + .toDataConnectResult(variables) override fun close() { logger.debug { "close() called" } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 3743cb4c661..73c640f025b 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -13,6 +13,8 @@ // limitations under the License. package com.google.firebase.dataconnect +import com.google.firebase.dataconnect.DataConnectGrpcClient.DeserialzedOperationResult +import com.google.firebase.dataconnect.DataConnectGrpcClient.OperationResult import com.google.protobuf.Struct import java.util.concurrent.CopyOnWriteArrayList import kotlinx.coroutines.CoroutineScope @@ -56,11 +58,6 @@ internal class QueryManager(grpcClient: DataConnectGrpcClient, coroutineScope: C it.collectExceptions(ref.dataDeserializer, collector) } } - - private companion object { - fun QueryState.ExecuteResult.toDataConnectResult(variables: V) = - DataConnectResult(variables = variables, data = data, errors = errors) - } } private data class QueryStateKey(val operationName: String, val variablesSha512: String) @@ -73,12 +70,12 @@ private class QueryState( private val coroutineScope: CoroutineScope, ) { private val mutex = Mutex() - private var job: Deferred? = null + private var job: Deferred? = null private val dataDeserializers = CopyOnWriteArrayList>() private val operationResultFlow = - MutableSharedFlow( + MutableSharedFlow( replay = 1, extraBufferCapacity = Int.MAX_VALUE, onBufferOverflow = BufferOverflow.SUSPEND @@ -91,9 +88,9 @@ private class QueryState( onBufferOverflow = BufferOverflow.SUSPEND ) - data class ExecuteResult(val data: T, val errors: List) - - suspend fun execute(dataDeserializer: DeserializationStrategy): ExecuteResult { + suspend fun execute( + dataDeserializer: DeserializationStrategy + ): DeserialzedOperationResult { // Wait for the current job to complete (if any), and ignore its result. Waiting avoids running // multiple queries in parallel, which would not scale. val originalJob = mutex.withLock { job }?.also { it.join() } @@ -120,7 +117,7 @@ private class QueryState( return newJob.await().deserialize(dataDeserializer) } - private suspend fun doExecute(): DataConnectGrpcClient.OperationResult { + private suspend fun doExecute(): OperationResult { val executeQueryResult = kotlin.runCatching { grpcClient.executeQuery(operationName = operationName, variables = variables) @@ -144,7 +141,7 @@ private class QueryState( suspend fun collectResults( dataDeserializer: DeserializationStrategy, collector: FlowCollector, - mapResult: ExecuteResult.() -> R + mapResult: DeserialzedOperationResult.() -> R ) = mutex .withLock { registerDataDeserializer(dataDeserializer) } @@ -171,21 +168,9 @@ private class QueryState( dataDeserializers.firstOrNull { it.deserializer === dataDeserializer } as? DeserialzerInfo ?: DeserialzerInfo(dataDeserializer).also { dataDeserializers.add(it) } - private companion object { - fun DataConnectGrpcClient.OperationResult.deserialize( - dataDeserializer: DeserializationStrategy - ): ExecuteResult { - if (data === null) { - // TODO: include the variables and error list in the thrown exception - throw DataConnectException("no data included in result: errors=${errors}") - } - return ExecuteResult(data = decodeFromStruct(dataDeserializer, data), errors = errors) - } - } - private class DeserialzerInfo(val deserializer: DeserializationStrategy) { private val _resultFlow = - MutableSharedFlow>( + MutableSharedFlow>( replay = 1, extraBufferCapacity = Int.MAX_VALUE, onBufferOverflow = BufferOverflow.SUSPEND, @@ -202,7 +187,7 @@ private class QueryState( val exceptionFlow = _exceptionFlow.asSharedFlow() - suspend fun update(result: Result) { + suspend fun update(result: Result) { result.fold( onSuccess = { val deserializeResult = kotlin.runCatching { it.deserialize(deserializer) } From 73fb8c049bb9db52a30dc0e3d2bc1d90d4bdb507 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 28 Nov 2023 14:26:29 -0500 Subject: [PATCH 086/573] QueryRefIntegrationTest.kt: add tests for lists (#518) --- .../testing_graphql_schemas/alltypes/ops.gql | 77 +++++------ .../alltypes/schema.gql | 13 ++ .../dataconnect/QueryRefIntegrationTest.kt | 98 ++++++++++---- .../testutil/schemas/AllTypesSchema.kt | 127 ++++++++++-------- 4 files changed, 184 insertions(+), 131 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql index bfa558d8f7a..a01e7b196ed 100644 --- a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql @@ -16,42 +16,6 @@ mutation createPrimitive($data: Primitive_Data! @pick(fields: ["id", "idFieldNul primitive_insert(data: $data) } -mutation deletePrimitive($id: ID!) @auth(is: PUBLIC) { - primitive_delete(id: $id) -} - -mutation updateIntField($id: ID!, $data: Primitive_Data! @pick(fields: ["intField"])) @auth(is: PUBLIC) { - primitive_update(id: $id, data: $data) -} - -mutation updateIntFieldNullable($id: ID!, $data: Primitive_Data! @pick(fields: ["intFieldNullable"])) @auth(is: PUBLIC) { - primitive_update(id: $id, data: $data) -} - -mutation updateFloatField($id: ID!, $data: Primitive_Data! @pick(fields: ["floatField"])) @auth(is: PUBLIC) { - primitive_update(id: $id, data: $data) -} - -mutation updateFloatFieldNullable($id: ID!, $data: Primitive_Data! @pick(fields: ["floatFieldNullable"])) @auth(is: PUBLIC) { - primitive_update(id: $id, data: $data) -} - -mutation updateBooleanField($id: ID!, $data: Primitive_Data! @pick(fields: ["booleanField"])) @auth(is: PUBLIC) { - primitive_update(id: $id, data: $data) -} - -mutation updateBooleanFieldNullable($id: ID!, $data: Primitive_Data! @pick(fields: ["booleanFieldNullable"])) @auth(is: PUBLIC) { - primitive_update(id: $id, data: $data) -} - -mutation updateStringField($id: ID!, $data: Primitive_Data! @pick(fields: ["stringField"])) @auth(is: PUBLIC) { - primitive_update(id: $id, data: $data) -} - -mutation updateStringFieldNullable($id: ID!, $data: Primitive_Data! @pick(fields: ["stringFieldNullable"])) @auth(is: PUBLIC) { - primitive_update(id: $id, data: $data) -} - query getPrimitive($id: ID!) @auth(is: PUBLIC) { primitive(id: $id) { id @@ -67,17 +31,36 @@ query getPrimitive($id: ID!) @auth(is: PUBLIC) { } } -query getAllPrimitives @auth(is: PUBLIC) { - primitives { +mutation createPrimitiveList($data: PrimitiveList_Data! @pick(fields: ["id", "idListNullable", "intList", "intListNullable", "floatList", "floatListNullable", "booleanList", "booleanListNullable", "stringList", "stringListNullable"])) @auth(is: PUBLIC) { + primitiveList_insert(data: $data) +} + +query getPrimitiveList($id: ID!) @auth(is: PUBLIC) { + primitiveList(id: $id) { id - idFieldNullable - intField - intFieldNullable - floatField - floatFieldNullable - booleanField - booleanFieldNullable - stringField - stringFieldNullable + idListNullable + intList + intListNullable + floatList + floatListNullable + booleanList + booleanListNullable + stringList + stringListNullable + } +} + +query getAllPrimitiveLists @auth(is: PUBLIC) { + primitiveLists { + id + idListNullable + intList + intListNullable + floatList + floatListNullable + booleanList + booleanListNullable + stringList + stringListNullable } } diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql index 53f8a85a920..e800bb736a2 100644 --- a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql @@ -24,3 +24,16 @@ type Primitive @table { stringField: String! stringFieldNullable: String } + +type PrimitiveList @table { + id: ID! + idListNullable: [ID] + intList: [Int!] + intListNullable: [Int] + floatList: [Float!] + floatListNullable: [Float] + booleanList: [Boolean!] + booleanListNullable: [Boolean] + stringList: [String!] + stringListNullable: [String] +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt index 2afd5b13e96..af0c00d2740 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -12,15 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -@file:OptIn(FlowPreview::class) - package com.google.firebase.dataconnect import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory -import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.CreatePrimitiveMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema +import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.GetAllPrimitiveListsQuery.execute +import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.GetPrimitiveListQuery.execute import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.GetPrimitiveQuery.execute import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation.execute import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetAllPeopleQuery.Data.Person @@ -100,21 +100,25 @@ class QueryRefIntegrationTest { @Test fun executeWithAllPrimitiveGraphQLTypesInDataNoneNull() = runTest { allTypesSchema.createPrimitive.execute( - id = "TestId", - idFieldNullable = "TestNullableId", - intField = 42, - intFieldNullable = 43, - floatField = 123.45f, - floatFieldNullable = 678.91f, - booleanField = true, - booleanFieldNullable = false, - stringField = "TestString", - stringFieldNullable = "TestNullableString", + AllTypesSchema.CreatePrimitiveMutation.Variables( + AllTypesSchema.PrimitiveData( + id = "TestId", + idFieldNullable = "TestNullableId", + intField = 42, + intFieldNullable = 43, + floatField = 123.45f, + floatFieldNullable = 678.91f, + booleanField = true, + booleanFieldNullable = false, + stringField = "TestString", + stringFieldNullable = "TestNullableString" + ) + ) ) val result = allTypesSchema.getPrimitive.execute(id = "TestId") - val primitive = result.data.primitive ?: error("result.data was null, but expected non") + val primitive = result.data.primitive ?: error("result.data.primitive is null") assertThat(primitive.id).isEqualTo("TestId") assertThat(primitive.idFieldNullable).isEqualTo("TestNullableId") assertThat(primitive.intField).isEqualTo(42) @@ -125,30 +129,74 @@ class QueryRefIntegrationTest { assertThat(primitive.booleanFieldNullable).isEqualTo(false) assertThat(primitive.stringField).isEqualTo("TestString") assertThat(primitive.stringFieldNullable).isEqualTo("TestNullableString") + assertThat(result.errors).isEmpty() } @Test fun executeWithAllPrimitiveGraphQLTypesInDataNullablesAreNull() = runTest { allTypesSchema.createPrimitive.execute( - id = "TestId", - idFieldNullable = null, - intField = 42, - intFieldNullable = null, - floatField = 123.45f, - floatFieldNullable = null, - booleanField = true, - booleanFieldNullable = null, - stringField = "TestString", - stringFieldNullable = null, + AllTypesSchema.CreatePrimitiveMutation.Variables( + AllTypesSchema.PrimitiveData( + id = "TestId", + idFieldNullable = null, + intField = 42, + intFieldNullable = null, + floatField = 123.45f, + floatFieldNullable = null, + booleanField = true, + booleanFieldNullable = null, + stringField = "TestString", + stringFieldNullable = null + ) + ) ) val result = allTypesSchema.getPrimitive.execute(id = "TestId") - val primitive = result.data.primitive ?: error("result.data was null, but expected non") + val primitive = result.data.primitive ?: error("result.data.primitive is null") assertThat(primitive.idFieldNullable).isNull() assertThat(primitive.intFieldNullable).isNull() assertThat(primitive.floatFieldNullable).isNull() assertThat(primitive.booleanFieldNullable).isNull() assertThat(primitive.stringFieldNullable).isNull() + assertThat(result.errors).isEmpty() + } + + @Test + fun executeWithAllListOfPrimitiveGraphQLTypesInData() = runTest { + // NOTE: `null` list elements (a.k.a. "sparse arrays") are not supported: b/300331607 + allTypesSchema.createPrimitiveList.execute( + AllTypesSchema.CreatePrimitiveListMutation.Variables( + AllTypesSchema.PrimitiveListData( + id = "TestId", + idListNullable = listOf("ddd", "eee"), + intList = listOf(42, 43, 44), + intListNullable = listOf(45, 46), + floatList = listOf(12.3f, 45.6f, 78.9f), + floatListNullable = listOf(98.7f, 65.4f), + booleanList = listOf(true, false, true, false), + booleanListNullable = listOf(false, true, false, true), + stringList = listOf("xxx", "yyy", "zzz"), + stringListNullable = listOf("qqq", "rrr"), + ) + ) + ) + + allTypesSchema.getAllPrimitiveLists.execute() + + val result = allTypesSchema.getPrimitiveList.execute(id = "TestId") + + val primitive = result.data.primitiveList ?: error("result.data.primitiveList is null") + assertThat(primitive.id).isEqualTo("TestId") + assertThat(primitive.idListNullable).containsExactly("ddd", "eee").inOrder() + assertThat(primitive.intList).containsExactly(42, 43, 44).inOrder() + assertThat(primitive.intListNullable).containsExactly(45, 46).inOrder() + assertThat(primitive.floatList).containsExactly(12.3f, 45.6f, 78.9f).inOrder() + assertThat(primitive.floatListNullable).containsExactly(98.7f, 65.4f).inOrder() + assertThat(primitive.booleanList).containsExactly(true, false, true, false).inOrder() + assertThat(primitive.booleanListNullable).containsExactly(false, true, false, true).inOrder() + assertThat(primitive.stringList).containsExactly("xxx", "yyy", "zzz").inOrder() + assertThat(primitive.stringListNullable).containsExactly("qqq", null, "rrr", null).inOrder() + assertThat(result.errors).isEmpty() } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt index 8f23ca53a2a..0bd83bd07df 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt @@ -15,7 +15,6 @@ package com.google.firebase.dataconnect.testutil.schemas import com.google.firebase.dataconnect.FirebaseDataConnect -import com.google.firebase.dataconnect.MutationRef import com.google.firebase.dataconnect.QueryRef import com.google.firebase.dataconnect.mutation import com.google.firebase.dataconnect.query @@ -38,50 +37,22 @@ class AllTypesSchema(private val dataConnect: FirebaseDataConnect) { dataConnect.installEmulatorSchema("testing_graphql_schemas/alltypes") } + @Serializable + data class PrimitiveData( + val id: String, + val idFieldNullable: String?, + val intField: Int, + val intFieldNullable: Int?, + val floatField: Float, + val floatFieldNullable: Float?, + val booleanField: Boolean, + val booleanFieldNullable: Boolean?, + val stringField: String, + val stringFieldNullable: String?, + ) + object CreatePrimitiveMutation { - @Serializable - data class PrimitiveData( - val id: String, - val idFieldNullable: String?, - val intField: Int, - val intFieldNullable: Int?, - val floatField: Float, - val floatFieldNullable: Float?, - val booleanField: Boolean, - val booleanFieldNullable: Boolean?, - val stringField: String, - val stringFieldNullable: String?, - ) @Serializable data class Variables(val data: PrimitiveData) - - suspend fun MutationRef.execute( - id: String, - idFieldNullable: String?, - intField: Int, - intFieldNullable: Int?, - floatField: Float, - floatFieldNullable: Float?, - booleanField: Boolean, - booleanFieldNullable: Boolean?, - stringField: String, - stringFieldNullable: String?, - ) = - execute( - Variables( - PrimitiveData( - id = id, - idFieldNullable = idFieldNullable, - intField = intField, - intFieldNullable = intFieldNullable, - floatField = floatField, - floatFieldNullable = floatFieldNullable, - booleanField = booleanField, - booleanFieldNullable = booleanFieldNullable, - stringField = stringField, - stringFieldNullable = stringFieldNullable, - ) - ) - ) } val createPrimitive = @@ -94,22 +65,7 @@ class AllTypesSchema(private val dataConnect: FirebaseDataConnect) { object GetPrimitiveQuery { @Serializable data class Variables(val id: String) - @Serializable - data class Data(val primitive: PrimitiveData?) { - @Serializable - data class PrimitiveData( - val id: String, - val idFieldNullable: String?, - val intField: Int, - val intFieldNullable: Int?, - val floatField: Float, - val floatFieldNullable: Float?, - val booleanField: Boolean, - val booleanFieldNullable: Boolean?, - val stringField: String, - val stringFieldNullable: String?, - ) - } + @Serializable data class Data(val primitive: PrimitiveData?) suspend fun QueryRef.execute(id: String) = execute(Variables(id = id)) } @@ -121,6 +77,59 @@ class AllTypesSchema(private val dataConnect: FirebaseDataConnect) { dataDeserializer = serializer() ) + @Serializable + data class PrimitiveListData( + val id: String, + val idListNullable: List, + val intList: List, + val intListNullable: List, + val floatList: List, + val floatListNullable: List, + val booleanList: List, + val booleanListNullable: List, + val stringList: List, + val stringListNullable: List, + ) + + object CreatePrimitiveListMutation { + @Serializable data class Variables(val data: PrimitiveListData) + } + + val createPrimitiveList = + dataConnect.mutation( + operationName = "createPrimitiveList", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + + object GetPrimitiveListQuery { + @Serializable data class Variables(val id: String) + + @Serializable data class Data(val primitiveList: PrimitiveListData?) + + suspend fun QueryRef.execute(id: String) = execute(Variables(id = id)) + } + + val getPrimitiveList = + dataConnect.query( + operationName = "getPrimitiveList", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + + object GetAllPrimitiveListsQuery { + @Serializable data class Data(val primitiveLists: List) + + suspend fun QueryRef.execute() = execute(Unit) + } + + val getAllPrimitiveLists = + dataConnect.query( + operationName = "getAllPrimitiveLists", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + companion object { const val OPERATION_SET = "ops" From 9d4fc51700fb52ff94fc46c72b3b0e36dc83123b Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 28 Nov 2023 14:49:41 -0500 Subject: [PATCH 087/573] DataConnectResult.kt: fix link to https://spec.graphql.org/draft/#sec-Errors --- .../kotlin/com/google/firebase/dataconnect/DataConnectResult.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt index 5c75067c572..9cef48dd8b6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt @@ -41,7 +41,7 @@ private constructor(private val impl: Impl) { ) } -// See https://spec.graphql.org/October2021/#sec-Errors +// See https://spec.graphql.org/draft/#sec-Errors class DataConnectError private constructor(private val impl: Impl) { internal constructor( From eae53d49985e6f427aa1faea898c07c753f3f8ea Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 28 Nov 2023 15:01:47 -0500 Subject: [PATCH 088/573] QueryRefIntegrationTest.kt: oops, fix test failure due to typo --- .../com/google/firebase/dataconnect/QueryRefIntegrationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt index af0c00d2740..3b410f51c72 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -196,7 +196,7 @@ class QueryRefIntegrationTest { assertThat(primitive.booleanList).containsExactly(true, false, true, false).inOrder() assertThat(primitive.booleanListNullable).containsExactly(false, true, false, true).inOrder() assertThat(primitive.stringList).containsExactly("xxx", "yyy", "zzz").inOrder() - assertThat(primitive.stringListNullable).containsExactly("qqq", null, "rrr", null).inOrder() + assertThat(primitive.stringListNullable).containsExactly("qqq", "rrr").inOrder() assertThat(result.errors).isEmpty() } } From cde5fb8c7abd30d150c740e3ee0966c17daac8df Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 28 Nov 2023 22:35:41 -0500 Subject: [PATCH 089/573] Add a CoroutineExceptionHandler to avoid crashing --- .../com/google/firebase/dataconnect/FirebaseDataConnect.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 7e3210d2dde..e7d008275ef 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -21,6 +21,7 @@ import com.google.firebase.app import com.google.firebase.concurrent.FirebaseExecutors import java.io.Closeable import java.util.concurrent.Executor +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -56,7 +57,10 @@ internal constructor( CoroutineScope( SupervisorJob() + nonBlockingExecutor.asCoroutineDispatcher() + - CoroutineName("FirebaseDataConnect") + CoroutineName("FirebaseDataConnect") + + CoroutineExceptionHandler { coroutineContext, throwable -> + logger.warn(throwable) { "uncaught exception from a coroutine" } + } ) // Dispatcher used to access `this.closed` and `this.grpcClient`. From ce3faf9750bee0dbf6cf0ebe2cb996dd1b6787cd Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 28 Nov 2023 22:35:57 -0500 Subject: [PATCH 090/573] use runCatching a little differently --- .../kotlin/com/google/firebase/dataconnect/QueryManager.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 73c640f025b..a39cf3298c5 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -119,9 +119,7 @@ private class QueryState( private suspend fun doExecute(): OperationResult { val executeQueryResult = - kotlin.runCatching { - grpcClient.executeQuery(operationName = operationName, variables = variables) - } + grpcClient.runCatching { executeQuery(operationName = operationName, variables = variables) } mutex.withLock { dataDeserializers.iterator() }.forEach { it.update(executeQueryResult) } From 55e952ec875adeeb6099bbb7d681c62c81db1a18 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 29 Nov 2023 02:04:32 -0500 Subject: [PATCH 091/573] close() changed to use a mutex rather than a synchronized executor --- .../dataconnect/FirebaseDataConnect.kt | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index e7d008275ef..6df562b7543 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -18,7 +18,6 @@ import android.content.Context import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app -import com.google.firebase.concurrent.FirebaseExecutors import java.io.Closeable import java.util.concurrent.Executor import kotlinx.coroutines.CoroutineExceptionHandler @@ -28,7 +27,8 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy @@ -63,20 +63,17 @@ internal constructor( } ) - // Dispatcher used to access `this.closed` and `this.grpcClient`. - private val sequentialDispatcher = - FirebaseExecutors.newSequentialExecutor(nonBlockingExecutor).asCoroutineDispatcher() + // Protects `closed`, `grpcClient`, and `queryManager`. + private val mutex = Mutex() - // This boolean value MUST only be accessed from code running on `sequentialDispatcher`. + // All accesses to this variable _must_ have locked `mutex`. private var closed = false - // This reference MUST only be set or dereferenced from code running on `sequentialDispatcher`. - private val grpcClient: DataConnectGrpcClient by lazy { - logger.debug { "DataConnectGrpcClient initialization started" } - if (closed) { - throw IllegalStateException("instance has been closed") - } - DataConnectGrpcClient( + // All accesses to this variable _must_ have locked `mutex`. + private val grpcClient = + lazy(LazyThreadSafetyMode.NONE) { + if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") + DataConnectGrpcClient( context = context, projectId = projectId, serviceId = serviceConfig.serviceId, @@ -89,28 +86,21 @@ internal constructor( executor = blockingExecutor, creatorLoggerId = logger.id, ) - .also { logger.debug { "DataConnectGrpcClient initialization complete: $it" } } - } + } - // This reference MUST only be set or dereferenced from code running on `sequentialDispatcher`. - private val queryManager: QueryManager by lazy { - if (closed) { - throw IllegalStateException("instance has been closed") + // All accesses to this variable _must_ have locked `mutex`. + private val queryManager = + lazy(LazyThreadSafetyMode.NONE) { + if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") + QueryManager(grpcClient.value, coroutineScope) } - QueryManager(grpcClient, coroutineScope) - } - internal suspend fun executeQuery( - ref: QueryRef, - variables: V - ): DataConnectResult = - withContext(sequentialDispatcher) { queryManager }.execute(ref, variables) - - internal suspend fun executeMutation( - ref: MutationRef, - variables: V - ): DataConnectResult = - withContext(sequentialDispatcher) { grpcClient } + internal suspend fun executeQuery(ref: QueryRef, variables: V) = + mutex.withLock { queryManager.value }.execute(ref, variables) + + internal suspend fun executeMutation(ref: MutationRef, variables: V) = + mutex + .withLock { grpcClient.value } .executeMutation( operationName = ref.operationName, variables = encodeToStruct(ref.variablesSerializer, variables) @@ -119,19 +109,31 @@ internal constructor( .toDataConnectResult(variables) override fun close() { - logger.debug { "close() called" } - runBlocking(sequentialDispatcher) { - if (!closed) { - doClose() - closed = true - } - } + // Set the `closed` flag to `true`, making sure to honor the requirement that `closed` is always + // accessed by a coroutine that has acquired `mutex`. + runBlocking { mutex.withLock { closed = true } } + + // Perform the actual close operations. Use (abuse?) a `Lazy` since it provides the exact + // semantics that we want, namely that (1) all invocations of `close()` will block, waiting for + // the close to complete, (2) if an exception is thrown by the close operation then that + // exception will be thrown to the caller and the next call of close() will try again, and (3) + // after successfully performing the close operations all subsequent calls are no-ops. + closeLazy.value } - private fun doClose() { - grpcClient.close() + private val closeLazy = lazy { + logger.debug { "Closing FirebaseDataConnect started" } + + creator.remove(this) + + val grpcClient = runBlocking { + mutex.withLock { if (grpcClient.isInitialized()) grpcClient.value else null } + } + grpcClient?.close() + coroutineScope.cancel() - creator.remove(this@FirebaseDataConnect) + + logger.debug { "Closing FirebaseDataConnect completed" } } override fun toString() = From 94dd6ac0601a0e70d03579296dcd809685c7d00c Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 29 Nov 2023 02:13:37 -0500 Subject: [PATCH 092/573] FirebaseDataConnect.kt: further clean up close() operation. --- .../dataconnect/FirebaseDataConnect.kt | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 6df562b7543..039a6d48a9e 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -20,6 +20,7 @@ import com.google.firebase.FirebaseApp import com.google.firebase.app import java.io.Closeable import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope @@ -108,32 +109,40 @@ internal constructor( .deserialize(ref.dataDeserializer) .toDataConnectResult(variables) - override fun close() { - // Set the `closed` flag to `true`, making sure to honor the requirement that `closed` is always - // accessed by a coroutine that has acquired `mutex`. - runBlocking { mutex.withLock { closed = true } } - - // Perform the actual close operations. Use (abuse?) a `Lazy` since it provides the exact - // semantics that we want, namely that (1) all invocations of `close()` will block, waiting for - // the close to complete, (2) if an exception is thrown by the close operation then that - // exception will be thrown to the caller and the next call of close() will try again, and (3) - // after successfully performing the close operations all subsequent calls are no-ops. - closeLazy.value - } + private val closeCompleted = AtomicBoolean(false) - private val closeLazy = lazy { - logger.debug { "Closing FirebaseDataConnect started" } - - creator.remove(this) + override fun close() { + // Short circuit: just return if the "close" operation has already completed. + if (closeCompleted.get()) { + return + } + // Set the `closed` flag to `true`, making sure to honor the requirement that `closed` is always + // accessed by a coroutine that has acquired `mutex`. Also, grab the `grpcClient` reference (if + // it was initialized), since that reference _also_ may only be accessed by a coroutine that + // has acquired `mutex`. val grpcClient = runBlocking { - mutex.withLock { if (grpcClient.isInitialized()) grpcClient.value else null } + mutex.withLock { + closed = true + if (grpcClient.isInitialized()) grpcClient.value else null + } } - grpcClient?.close() - coroutineScope.cancel() + // Do the "close" operation. Make sure to check `closeCompleted` again, since another thread may + // have beat us here and done the "close" operation already. + synchronized(closeCompleted) { + if (closeCompleted.get()) { + return + } - logger.debug { "Closing FirebaseDataConnect completed" } + logger.debug { "Closing FirebaseDataConnect started" } + creator.remove(this) + grpcClient?.close() + coroutineScope.cancel() + logger.debug { "Closing FirebaseDataConnect completed" } + + closeCompleted.set(true) + } } override fun toString() = From 6e8196b12d59aad4042529bda05b8499e4e86914 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 29 Nov 2023 10:37:18 -0500 Subject: [PATCH 093/573] QueryRefIntegrationTest.kt: add tests for invoking execute() after close and for massive concurrency --- .../dataconnect/QueryRefIntegrationTest.kt | 34 +++++++++++++++++++ .../testutil/schemas/AllTypesSchema.kt | 2 +- .../testutil/schemas/PersonSchema.kt | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt index 3b410f51c72..0d67769022a 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -16,6 +16,7 @@ package com.google.firebase.dataconnect import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema @@ -27,6 +28,7 @@ import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetAllPeopl import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetAllPeopleQuery.execute import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.execute import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.test.* @@ -199,4 +201,36 @@ class QueryRefIntegrationTest { assertThat(primitive.stringListNullable).containsExactly("qqq", "rrr").inOrder() assertThat(result.errors).isEmpty() } + + @Test + fun executeShouldThrowIfDataConnectInstanceIsClosed() = runTest { + personSchema.dataConnect.close() + + val result = personSchema.getPerson.runCatching { execute(id = "foo") } + + assertWithMessage("result=${result.getOrNull()}").that(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun executeShouldSupportMassiveConcurrency() = + runTest(timeout = 60.seconds) { + val queryRef = personSchema.getPerson + + val deferreds = buildList { + repeat(25_000) { + // Use `Dispatchers.Default` as the dispatcher for the launched coroutines so that there + // will be at least 2 threads used to run the coroutines (as documented by + // `Dispatchers.Default`), introducing a guaranteed minimum level of parallelism, ensuring + // that this test is indeed testing "massive concurrency". + add(backgroundScope.async(Dispatchers.Default) { queryRef.execute(id = "foo") }) + } + } + + val results = deferreds.map { it.await() } + results.forEachIndexed { index, result -> + assertWithMessage("results[$index]").that(result.data.person).isNull() + assertWithMessage("results[$index]").that(result.errors).isEmpty() + } + } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt index 0bd83bd07df..2d8e664c61e 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt @@ -23,7 +23,7 @@ import com.google.firebase.dataconnect.testutil.installEmulatorSchema import kotlinx.serialization.Serializable import kotlinx.serialization.serializer -class AllTypesSchema(private val dataConnect: FirebaseDataConnect) { +class AllTypesSchema(val dataConnect: FirebaseDataConnect) { init { dataConnect.serviceConfig.operationSet.let { diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index 4e4f2801903..bd6b7ab154f 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -24,7 +24,7 @@ import com.google.firebase.dataconnect.testutil.installEmulatorSchema import kotlinx.serialization.Serializable import kotlinx.serialization.serializer -class PersonSchema(private val dataConnect: FirebaseDataConnect) { +class PersonSchema(val dataConnect: FirebaseDataConnect) { init { dataConnect.serviceConfig.operationSet.let { From e259504ec2da587155fff5030777beab1574c53b Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 29 Nov 2023 12:44:56 -0500 Subject: [PATCH 094/573] FirebaseDataConnectFactory.kt cleanup --- .../dataconnect/FirebaseDataConnectFactory.kt | 146 +++++++++++------- 1 file changed, 87 insertions(+), 59 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt index bf5c5f0e7af..58996692343 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt @@ -15,7 +15,6 @@ package com.google.firebase.dataconnect import android.content.Context import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseAppLifecycleListener import java.util.concurrent.Executor import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -27,19 +26,12 @@ internal class FirebaseDataConnectFactory( private val nonBlockingExecutor: Executor, ) { - private val firebaseAppLifecycleListener = FirebaseAppLifecycleListener { _, _ -> close() } init { - firebaseApp.addLifecycleEventListener(firebaseAppLifecycleListener) + firebaseApp.addLifecycleEventListener { _, _ -> close() } } - private data class InstanceCacheKey( - val serviceId: String, - val location: String, - val operationSet: String, - ) - private val lock = ReentrantLock() - private val instancesByCacheKey = mutableMapOf() + private val instances = mutableMapOf() private var closed = false fun get( @@ -48,82 +40,118 @@ internal class FirebaseDataConnectFactory( ): FirebaseDataConnect { val key = serviceConfig.run { - InstanceCacheKey(serviceId = serviceId, location = location, operationSet = operationSet) + FirebaseDataConnectInstanceKey( + serviceId = serviceId, + location = location, + operationSet = operationSet + ) } + lock.withLock { if (closed) { throw IllegalStateException("FirebaseApp has been deleted") } - val cachedInstance = instancesByCacheKey[key] + + val cachedInstance = instances[key] if (cachedInstance !== null) { - if (settings !== null && settings != cachedInstance.settings) { - throw IllegalArgumentException( - "The cached FirebaseDataConnect instance ($cachedInstance)" + - " must have the same settings as the specified settings; however, they are different" + - " (cached settings: ${cachedInstance.settings}, specified settings: $settings)" - ) - } - if (serviceConfig.revision != cachedInstance.serviceConfig.revision) { - throw IllegalArgumentException( - "The cached FirebaseDataConnect instance ($cachedInstance)" + - " must have the same 'revision' as the specified ServiceConfig; however, they are" + - " different (cached revision: ${cachedInstance.serviceConfig.revision}," + - " specified revision: ${serviceConfig.revision})" - ) - } + throwIfIncompatible(key, cachedInstance, serviceConfig, settings) return cachedInstance } - val projectId = firebaseApp.options.projectId ?: "" - val newInstance = - FirebaseDataConnect( - context = context, - app = firebaseApp, - projectId = projectId, - serviceConfig = serviceConfig, - blockingExecutor = blockingExecutor, - nonBlockingExecutor = nonBlockingExecutor, - creator = this, - settings = settings ?: FirebaseDataConnectSettings.defaults - ) - instancesByCacheKey[key] = newInstance + val newInstance = FirebaseDataConnect.newInstance(serviceConfig, settings) + instances[key] = newInstance return newInstance } } + private fun FirebaseDataConnect.Companion.newInstance( + serviceConfig: FirebaseDataConnect.ServiceConfig, + settings: FirebaseDataConnectSettings? + ) = + FirebaseDataConnect( + context = context, + app = firebaseApp, + projectId = firebaseApp.options.projectId ?: "", + serviceConfig = serviceConfig, + blockingExecutor = blockingExecutor, + nonBlockingExecutor = nonBlockingExecutor, + creator = this@FirebaseDataConnectFactory, + settings = settings ?: FirebaseDataConnectSettings.defaults, + ) + fun remove(instance: FirebaseDataConnect) { lock.withLock { - val entries = instancesByCacheKey.entries.filter { it.value === instance } - if (entries.isEmpty()) { - return - } else if (entries.size == 1) { - instancesByCacheKey.remove(entries[0].key) - } else { - throw IllegalStateException( - "internal error: FirebaseDataConnect instance $instance" + - "maps to more than one key: ${entries.map { it.key }.joinToString(", ")}" - ) + val keysForInstance = instances.entries.filter { it.value === instance }.map { it.key } + + when (keysForInstance.size) { + 0 -> {} + 1 -> instances.remove(keysForInstance[0]) + else -> + throw IllegalStateException( + "internal error: FirebaseDataConnect instance $instance " + + "maps to ${keysForInstance.size} keys, but expected at most 1: " + + keysForInstance.joinToString(", ") + ) } } } private fun close() { - val instances = mutableListOf() - lock.withLock { - closed = true - instances.addAll(instancesByCacheKey.values) - } + val instanceList = + lock.withLock { + closed = true + instances.values.toList() + } - instances.forEach { instance -> instance.close() } + instanceList.forEach(FirebaseDataConnect::close) lock.withLock { - if (instancesByCacheKey.isNotEmpty()) { + if (instances.isNotEmpty()) { throw IllegalStateException( - "instances contains ${instances.size} instances " + - "after calling terminate() on all FirebaseDataConnect instances, " + + "internal error: 'instances' contains ${instances.size} elements " + + "after calling close() on all FirebaseDataConnect instances, " + "but expected 0" ) } } } } + +private data class FirebaseDataConnectInstanceKey( + val serviceId: String, + val location: String, + val operationSet: String, +) { + override fun toString() = "serviceId=$serviceId, location=$location, operationSet=$operationSet" +} + +private fun throwIfIncompatible( + key: FirebaseDataConnectInstanceKey, + instance: FirebaseDataConnect, + serviceConfig: FirebaseDataConnect.ServiceConfig, + settings: FirebaseDataConnectSettings? +) { + val keyStr = key.run { "serviceId=$serviceId, location=$location, operationSet=$operationSet" } + + if (instance.serviceConfig.revision != serviceConfig.revision) { + throw IllegalArgumentException( + "The 'revision' of the FirebaseDataConnect instance with [$keyStr] is " + + "'${instance.serviceConfig.revision}', which is different from the " + + "'revision' of the given ServiceConfig: '${serviceConfig.revision}`; " + + "to get a FirebaseDataConnect with [$keyStr] but a different 'revision', first call " + + "close() on the existing FirebaseDataConnect instance, then call getInstance() again " + + "with the desired 'revision'." + ) + } + + if (settings !== null && instance.settings != settings) { + throw IllegalArgumentException( + "The settings of the FirebaseDataConnect instance with [$keyStr] is " + + "'${instance.settings}', which is different from the given settings: $settings; " + + "to get a FirebaseDataConnect with [$keyStr] but different settings, first call " + + "close() on the existing FirebaseDataConnect instance, then call getInstance() again " + + "with the desired settings. Alternately, call getInstance() with null settings to " + + "use whatever settings are configured in the existing FirebaseDataConnect instance." + ) + } +} From a08ce643fe9377df6351ff2844f2ad01ee5c285d Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 29 Nov 2023 12:45:22 -0500 Subject: [PATCH 095/573] Logger.kt: minor tweak to be more kotlin idiomatic --- .../src/main/kotlin/com/google/firebase/dataconnect/Logger.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt index e8198d97a40..e5c3757573a 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt @@ -75,7 +75,7 @@ private class LoggerImpl(private val name: String, private val idInt: Int) : Log override val id: String by lazy(LazyThreadSafetyMode.PUBLICATION) { - StringBuilder().run { + buildString { append(name) append('[') append("0x") @@ -83,7 +83,6 @@ private class LoggerImpl(private val name: String, private val idInt: Int) : Log repeat(8 - idHexString.length) { append('0') } append(idHexString) append(']') - toString() } } From eabb2556fa7f05d66a4023747bc0a5d513190466 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 29 Nov 2023 15:25:24 -0500 Subject: [PATCH 096/573] clean up close and logging IDs --- .../generated/PostsIntegrationTest.kt | 2 +- .../testutil/TestDataConnectFactory.kt | 1 + .../testutil/TestFirebaseAppFactory.kt | 1 + .../dataconnect/DataConnectGrpcClient.kt | 172 ++++++++++++------ .../dataconnect/FirebaseDataConnect.kt | 117 ++++++++---- .../com/google/firebase/dataconnect/Logger.kt | 24 +-- .../firebase/dataconnect/QueryManager.kt | 61 +++++-- .../com/google/firebase/dataconnect/Util.kt | 37 +++- .../FirebaseDataConnectSettingsTest.kt | 1 - .../dataconnect/testutil/TestUtils.kt | 29 --- 10 files changed, 291 insertions(+), 154 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt index 9a67ad92568..865c480e23b 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt @@ -19,9 +19,9 @@ import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.Firebase import com.google.firebase.app import com.google.firebase.dataconnect.FirebaseDataConnectSettings +import com.google.firebase.dataconnect.nextAlphanumericString import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory -import com.google.firebase.dataconnect.testutil.nextAlphanumericString import kotlin.random.Random import kotlinx.coroutines.* import kotlinx.coroutines.flow.* diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt index 9759283ecf0..7639f1ce394 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt @@ -16,6 +16,7 @@ package com.google.firebase.dataconnect.testutil import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.FirebaseDataConnectSettings +import com.google.firebase.dataconnect.nextAlphanumericString import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema.Companion.installAllTypesSchema import com.google.firebase.dataconnect.testutil.schemas.PersonSchema diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt index 95f1157d43b..54478c84bbd 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestFirebaseAppFactory.kt @@ -3,6 +3,7 @@ package com.google.firebase.dataconnect.testutil import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app +import com.google.firebase.dataconnect.nextAlphanumericString import com.google.firebase.initialize import kotlin.random.Random diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 13733ff70f7..1018554464e 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -27,6 +27,11 @@ import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder import java.util.concurrent.Executor import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.completeWith +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.serialization.DeserializationStrategy internal class DataConnectGrpcClient( @@ -39,67 +44,101 @@ internal class DataConnectGrpcClient( hostName: String, port: Int, sslEnabled: Boolean, - executor: Executor, + private val executor: Executor, creatorLoggerId: String, ) { - private val logger = Logger("DataConnectGrpcClient") + private val logger = + Logger("DataConnectGrpcClient").apply { debug { "Created from $creatorLoggerId" } } private val requestName = "projects/$projectId/locations/$location/services/$serviceId/" + "operationSets/$operationSet/revisions/$revision" - init { - logger.debug { "Created from $creatorLoggerId" } - } + // Protects `closed`, `grpcChannel`, and `grpcStub`. + private val mutex = Mutex() + + // All accesses to this variable _must_ have locked `mutex`. + private var closed = false + + // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference + // to the lazily-created object is obtained, then the mutex can be unlocked and the instance can + // be used. + private val grpcChannel = + lazy(LazyThreadSafetyMode.NONE) { + logger.debug { "${ManagedChannel::class.qualifiedName} initialization started" } + + if (closed) throw IllegalStateException("DataConnectGrpcClient instance has been closed") + + // Upgrade the Android security provider using Google Play Services. + // + // We need to upgrade the Security Provider before any network channels are initialized + // because + // okhttp maintains a list of supported providers that is initialized when the JVM first + // resolves the static dependencies of ManagedChannel. + // + // If initialization fails for any reason, then a warning is logged and the original, + // un-upgraded security provider is used. + try { + ProviderInstaller.installIfNeeded(context) + } catch (e: Exception) { + logger.warn(e) { "Failed to update ssl context" } + } - private val grpcChannel: ManagedChannel by lazy { - // Upgrade the Android security provider using Google Play Services. - // - // We need to upgrade the Security Provider before any network channels are initialized because - // okhttp maintains a list of supported providers that is initialized when the JVM first - // resolves the static dependencies of ManagedChannel. - // - // If initialization fails for any reason, then a warning is logged and the original, - // un-upgraded security provider is used. - try { - ProviderInstaller.installIfNeeded(context) - } catch (e: Exception) { - logger.warn(e) { "Failed to update ssl context" } - } + ManagedChannelBuilder.forAddress(hostName, port).let { + if (!sslEnabled) { + it.usePlaintext() + } - ManagedChannelBuilder.forAddress(hostName, port).let { - if (!sslEnabled) { - it.usePlaintext() - } + // Ensure gRPC recovers from a dead connection. This is not typically necessary, as + // the OS will usually notify gRPC when a connection dies. But not always. This acts as a + // failsafe. + it.keepAliveTime(30, TimeUnit.SECONDS) + + it.executor(executor) - // Ensure gRPC recovers from a dead connection. This is not typically necessary, as - // the OS will usually notify gRPC when a connection dies. But not always. This acts as a - // failsafe. - it.keepAliveTime(30, TimeUnit.SECONDS) + // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel + // to + // respond more gracefully to network change events, such as switching from cellular to + // wifi. + val channel = AndroidChannelBuilder.usingBuilder(it).context(context).build() - it.executor(executor) + logger.debug { "${ManagedChannel::class.qualifiedName} initialization completed" } - // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel to - // respond more gracefully to network change events, such as switching from cellular to wifi. - AndroidChannelBuilder.usingBuilder(it).context(context).build() + channel + } } - } - private val grpcStub: DataServiceCoroutineStub by lazy { DataServiceCoroutineStub(grpcChannel) } + private val grpcChannelOrNull + get() = if (grpcChannel.isInitialized()) grpcChannel.value else null + + // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference + // to the lazily-created object is obtained, then the mutex can be unlocked and the instance can + // be used. + private val grpcStub: DataServiceCoroutineStub by + lazy(LazyThreadSafetyMode.NONE) { DataServiceCoroutineStub(grpcChannel.value) } data class OperationResult(val data: Struct?, val errors: List) data class DeserialzedOperationResult(val data: T, val errors: List) - suspend fun executeQuery(operationName: String, variables: Struct): OperationResult { + suspend fun executeQuery( + requestId: String, + operationName: String, + variables: Struct + ): OperationResult { val request = executeQueryRequest { this.name = requestName this.operationName = operationName this.variables = variables } - logger.debug { "executeQuery() sending request: $request" } - val response = grpcStub.executeQuery(request) - logger.debug { "executeQuery() got response: $response" } + logger.debug { "executeQuery() requestId=$requestId sending: $request" } + val response = + mutex + .withLock { grpcStub } + .runCatching { executeQuery(request) } + .onFailure { logger.warn(it) { "executeQuery() requestId=$requestId grpc call FAILED" } } + .getOrThrow() + logger.debug { "executeQuery() requestId=$requestId got response: $response" } return OperationResult( data = if (response.hasData()) response.data else null, @@ -107,16 +146,25 @@ internal class DataConnectGrpcClient( ) } - suspend fun executeMutation(operationName: String, variables: Struct): OperationResult { + suspend fun executeMutation( + requestId: String, + operationName: String, + variables: Struct + ): OperationResult { val request = executeMutationRequest { this.name = requestName this.operationName = operationName this.variables = variables } - logger.debug { "executeMutation() sending request: $request" } - val response = grpcStub.executeMutation(request) - logger.debug { "executeMutation() got response: $response" } + logger.debug { "executeMutation() requestId=$requestId sending: $request" } + val response = + mutex + .withLock { grpcStub } + .runCatching { executeMutation(request) } + .onFailure { logger.warn(it) { "executeMutation() requestId=$requestId grpc call FAILED" } } + .getOrThrow() + logger.debug { "executeMutation() requestId=$requestId got response: $response" } return OperationResult( data = if (response.hasData()) response.data else null, @@ -124,27 +172,49 @@ internal class DataConnectGrpcClient( ) } - fun close() { - logger.debug { "close() starting" } - grpcChannel.shutdownNow() - logger.debug { "close() done" } + private val closingMutex = Mutex() + private var closeCompleted = false + + suspend fun close() { + val grpcChannel = + mutex.withLock { + closed = true + grpcChannelOrNull + } + + closingMutex.withLock { + if (!closeCompleted) { + grpcChannel?.terminate() + } + closeCompleted = true + } } -} -internal fun Struct.decode(deserializer: DeserializationStrategy) = - decodeFromStruct(deserializer, this) + private suspend fun ManagedChannel.terminate() { + shutdown() + + val deferred = CompletableDeferred() + thread(isDaemon = true, name = "ManagedChannel.terminate() from ${logger.id}") { + deferred.completeWith( + runCatching { awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS) } + ) + } + + deferred.await() + } +} -internal fun ListValue.decodePath() = +internal fun ListValue.toPathSegment() = valuesList.map { when (val kind = it.kindCase) { Value.KindCase.STRING_VALUE -> DataConnectError.PathSegment.Field(it.stringValue) Value.KindCase.NUMBER_VALUE -> DataConnectError.PathSegment.ListIndex(it.numberValue.toInt()) - else -> throw IllegalStateException("invalid PathSegement kind: $kind") + else -> DataConnectError.PathSegment.Field("invalid PathSegment kind: $kind") } } internal fun GraphqlError.toDataConnectError() = - DataConnectError(message = message, path = path.decodePath(), extensions = emptyMap()) + DataConnectError(message = message, path = path.toPathSegment(), extensions = emptyMap()) internal fun DataConnectGrpcClient.OperationResult.deserialize( dataDeserializer: DeserializationStrategy diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 039a6d48a9e..bdb813ac305 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -18,15 +18,20 @@ import android.content.Context import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app -import java.io.Closeable import java.util.concurrent.Executor -import java.util.concurrent.atomic.AtomicBoolean +import kotlin.random.Random import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -43,7 +48,7 @@ internal constructor( internal val nonBlockingExecutor: Executor, private val creator: FirebaseDataConnectFactory, val settings: FirebaseDataConnectSettings, -) : Closeable { +) : AutoCloseable { private val logger = Logger("FirebaseDataConnect").apply { @@ -70,7 +75,9 @@ internal constructor( // All accesses to this variable _must_ have locked `mutex`. private var closed = false - // All accesses to this variable _must_ have locked `mutex`. + // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference + // to the lazily-created object is obtained, then the mutex can be unlocked and the instance can + // be used. private val grpcClient = lazy(LazyThreadSafetyMode.NONE) { if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") @@ -89,59 +96,92 @@ internal constructor( ) } - // All accesses to this variable _must_ have locked `mutex`. + // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference + // to the lazily-created object is obtained, then the mutex can be unlocked and the instance can + // be used. + private val grpcClientOrNull + get() = if (grpcClient.isInitialized()) grpcClient.value else null + + // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference + // // to the lazily-created object is obtained, then the mutex can be unlocked and the instance + // can + // // be used. private val queryManager = lazy(LazyThreadSafetyMode.NONE) { if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") - QueryManager(grpcClient.value, coroutineScope) + QueryManager(grpcClient.value, coroutineScope, logger.id) } internal suspend fun executeQuery(ref: QueryRef, variables: V) = mutex.withLock { queryManager.value }.execute(ref, variables) internal suspend fun executeMutation(ref: MutationRef, variables: V) = + executeMutation(ref, variables, requestId = Random.nextAlphanumericString()) + + private suspend fun executeMutation( + ref: MutationRef, + variables: V, + requestId: String + ) = mutex .withLock { grpcClient.value } .executeMutation( + requestId = requestId, operationName = ref.operationName, variables = encodeToStruct(ref.variablesSerializer, variables) ) - .deserialize(ref.dataDeserializer) + .runCatching { deserialize(ref.dataDeserializer) } + .onFailure { + logger.warn(it) { "executeMutation() requestId=$requestId decoding response data failed" } + } + .getOrThrow() .toDataConnectResult(variables) - private val closeCompleted = AtomicBoolean(false) + private val closeResult = MutableStateFlow?>(null) override fun close() { - // Short circuit: just return if the "close" operation has already completed. - if (closeCompleted.get()) { - return - } + logger.debug { "close() called" } + // Remove the reference to this `FirebaseDataConnect` instance from the + // `FirebaseDataConnectFactory` that created it, so that the next time that `getInstance()` is + // called with the same arguments that a new instance of `FirebaseDataConnect` will be created. + creator.remove(this) // Set the `closed` flag to `true`, making sure to honor the requirement that `closed` is always - // accessed by a coroutine that has acquired `mutex`. Also, grab the `grpcClient` reference (if - // it was initialized), since that reference _also_ may only be accessed by a coroutine that - // has acquired `mutex`. - val grpcClient = runBlocking { - mutex.withLock { - closed = true - if (grpcClient.isInitialized()) grpcClient.value else null - } + // accessed by a coroutine that has acquired `mutex` + runBlocking { mutex.withLock { closed = true } } + + // If a previous attempt was successful, then just return because there is nothing to do. + if (closeResult.isResultSuccess) { + return } - // Do the "close" operation. Make sure to check `closeCompleted` again, since another thread may - // have beat us here and done the "close" operation already. - synchronized(closeCompleted) { - if (closeCompleted.get()) { + // Clear the result of the previous failed attempt, since we're about to try again. + closeResult.clearResultUnlessSuccess() + + // Launch an asynchronous coroutine to actually perform the remainder of the close operation, + // as it potentially suspends and this close() function is a "normal", non-suspending function. + @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch { doClose() } + } + + suspend fun awaitClose(): Unit = closeResult.filterNotNull().first().getOrThrow() + + private val closingMutex = Mutex() + + private suspend fun doClose() { + closingMutex.withLock { + if (closeResult.isResultSuccess) { return } - logger.debug { "Closing FirebaseDataConnect started" } - creator.remove(this) - grpcClient?.close() - coroutineScope.cancel() - logger.debug { "Closing FirebaseDataConnect completed" } - - closeCompleted.set(true) + closeResult.value = + kotlin + .runCatching { + logger.debug { "Closing started" } + mutex.withLock { grpcClientOrNull }?.close() + coroutineScope.cancel() + logger.debug { "Closing completed" } + } + .onFailure { logger.warn(it) { "Closing failed" } } } } @@ -202,6 +242,21 @@ internal constructor( settings: FirebaseDataConnectSettings? = null ): FirebaseDataConnect = getInstance(app = Firebase.app, serviceConfig = serviceConfig, settings = settings) + + private fun MutableStateFlow?>.clearResultUnlessSuccess() { + while (true) { + val oldValue = value + if (oldValue != null && oldValue.isSuccess) { + return + } + if (compareAndSet(oldValue, null)) { + return + } + } + } + + private val MutableStateFlow?>.isResultSuccess + get() = value?.isSuccess == true } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt index e5c3757573a..34be6415d63 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt @@ -14,7 +14,7 @@ package com.google.firebase.dataconnect import android.util.Log -import java.util.concurrent.atomic.AtomicInteger +import kotlin.random.Random enum class LogLevel { DEBUG, @@ -33,16 +33,10 @@ internal interface Logger { fun warn(e: Throwable?, message: () -> Any?) } -internal fun Logger(name: String): Logger = - LoggerImpl(name = name, idInt = nextLoggerId.getAndIncrement()) +internal fun Logger(name: String): Logger = LoggerImpl(name) private const val LOG_TAG = "FirebaseDataConnect" -// TODO: Use kotlin.concurrent.AtomicInt once kotlin-stdlib is upgraded to 1.9 -// The initial value is just an arbitrary, non-zero value so that logger IDs are easily searchable -// in logs due to the "uniqueness" of their first 4 digits. -private val nextLoggerId = AtomicInteger(0x591F0000) - private fun isLogEnabledFor(level: LogLevel) = when (logLevel) { LogLevel.DEBUG -> @@ -71,20 +65,10 @@ private fun runIfLogEnabled(level: LogLevel, block: () -> Unit) { } } -private class LoggerImpl(private val name: String, private val idInt: Int) : Logger { +private class LoggerImpl(private val name: String) : Logger { override val id: String by - lazy(LazyThreadSafetyMode.PUBLICATION) { - buildString { - append(name) - append('[') - append("0x") - val idHexString = idInt.toString(16) - repeat(8 - idHexString.length) { append('0') } - append(idHexString) - append(']') - } - } + lazy(LazyThreadSafetyMode.PUBLICATION) { "$name[id=${Random.nextAlphanumericString()}]" } override fun info(message: () -> Any?) = runIfLogEnabled(LogLevel.INFO) { Log.i(LOG_TAG, "$id ${message()}") } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index a39cf3298c5..aaf740828ef 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -17,6 +17,7 @@ import com.google.firebase.dataconnect.DataConnectGrpcClient.DeserialzedOperatio import com.google.firebase.dataconnect.DataConnectGrpcClient.OperationResult import com.google.protobuf.Struct import java.util.concurrent.CopyOnWriteArrayList +import kotlin.random.Random import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.NonCancellable @@ -31,8 +32,14 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.serialization.DeserializationStrategy -internal class QueryManager(grpcClient: DataConnectGrpcClient, coroutineScope: CoroutineScope) { - private val queryStates = QueryStates(grpcClient, coroutineScope) +internal class QueryManager( + grpcClient: DataConnectGrpcClient, + coroutineScope: CoroutineScope, + creatorLoggerId: String +) { + private val logger = Logger("QueryManager").apply { debug { "Created from $creatorLoggerId" } } + + private val queryStates = QueryStates(grpcClient, coroutineScope, logger) suspend fun execute(ref: QueryRef, variables: V): DataConnectResult = queryStates @@ -68,6 +75,7 @@ private class QueryState( private val operationName: String, private val variables: Struct, private val coroutineScope: CoroutineScope, + private val logger: Logger, ) { private val mutex = Mutex() private var job: Deferred? = null @@ -118,10 +126,16 @@ private class QueryState( } private suspend fun doExecute(): OperationResult { + val requestId = Random.nextAlphanumericString() + val executeQueryResult = - grpcClient.runCatching { executeQuery(operationName = operationName, variables = variables) } + grpcClient.runCatching { + executeQuery(requestId = requestId, operationName = operationName, variables = variables) + } - mutex.withLock { dataDeserializers.iterator() }.forEach { it.update(executeQueryResult) } + mutex + .withLock { dataDeserializers.iterator() } + .forEach { it.update(requestId, executeQueryResult) } return executeQueryResult.fold( onSuccess = { @@ -164,9 +178,12 @@ private class QueryState( dataDeserializer: DeserializationStrategy ): DeserialzerInfo = dataDeserializers.firstOrNull { it.deserializer === dataDeserializer } as? DeserialzerInfo - ?: DeserialzerInfo(dataDeserializer).also { dataDeserializers.add(it) } + ?: DeserialzerInfo(dataDeserializer, logger).also { dataDeserializers.add(it) } - private class DeserialzerInfo(val deserializer: DeserializationStrategy) { + private class DeserialzerInfo( + val deserializer: DeserializationStrategy, + private val logger: Logger + ) { private val _resultFlow = MutableSharedFlow>( replay = 1, @@ -185,19 +202,25 @@ private class QueryState( val exceptionFlow = _exceptionFlow.asSharedFlow() - suspend fun update(result: Result) { + suspend fun update(requestId: String, result: Result) { result.fold( - onSuccess = { - val deserializeResult = kotlin.runCatching { it.deserialize(deserializer) } - deserializeResult.fold( - onSuccess = { - _resultFlow.emit(it) - _exceptionFlow.emit(null) - }, - onFailure = { _exceptionFlow.emit(it) } - ) - }, onFailure = { _exceptionFlow.emit(it) }, + onSuccess = { operationResult -> + operationResult + .runCatching { deserialize(deserializer) } + .fold( + onSuccess = { deserializedOperationResult -> + _resultFlow.emit(deserializedOperationResult) + _exceptionFlow.emit(null) + }, + onFailure = { deserializeException -> + logger.warn(deserializeException) { + "executeQuery() requestId=$requestId decoding response data failed" + } + _exceptionFlow.emit(deserializeException) + } + ) + }, ) } } @@ -205,7 +228,8 @@ private class QueryState( private class QueryStates( private val grpcClient: DataConnectGrpcClient, - private val coroutineScope: CoroutineScope + private val coroutineScope: CoroutineScope, + private val logger: Logger, ) { private val mutex = Mutex() @@ -243,6 +267,7 @@ private class QueryStates( operationName = ref.operationName, variables = variablesStruct, coroutineScope = coroutineScope, + logger = logger, ), refCount = 0 ) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 5e191e97d93..26554e9f877 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -14,14 +14,45 @@ package com.google.firebase.dataconnect import java.io.OutputStream +import kotlin.math.abs +import kotlin.random.Random -fun ByteArray.toHexString(): String = +internal fun ByteArray.toHexString(): String = joinToString(separator = "") { it.toUByte().toInt().toString(16).padStart(2, '0') } -object NullOutputStream : OutputStream() { +internal object NullOutputStream : OutputStream() { override fun write(b: Int) {} override fun write(b: ByteArray?) {} override fun write(b: ByteArray?, off: Int, len: Int) {} } -class ReferenceCounted(val obj: T, var refCount: Int) +internal class ReferenceCounted(val obj: T, var refCount: Int) + +/** + * Generates and returns a string containing random alphanumeric characters. + * + * NOTE: The randomness of this function is NOT cryptographically safe. Only use the strings + * returned from this method in contexts where security is not a concern. + * + * @param length the number of random characters to generate and include in the returned string; if + * `null`, then a length of 10 is used. + * @return a string containing the given (or default) number of random alphanumeric characters. + */ +internal fun Random.nextAlphanumericString(length: Int? = null): String = buildString { + var numCharactersRemaining = + if (length === null) 10 else length.also { require(it >= 0) { "invalid length: $it" } } + + while (numCharactersRemaining > 0) { + // Ignore the first character of the alphanumeric string because its distribution is not random. + val randomCharacters = abs(nextLong()).toAlphaNumericString() + val numCharactersToAppend = kotlin.math.min(numCharactersRemaining, randomCharacters.length - 1) + append(randomCharacters, 1, numCharactersToAppend) + numCharactersRemaining -= numCharactersToAppend + } +} + +/** + * Converts this number to a base-36 string, which uses the 26 letters from the English alphabet and + * the 10 numeric digits. + */ +internal fun Long.toAlphaNumericString(): String = toString(36) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt index 8bfb7ee63a1..30d591632f1 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectSettingsTest.kt @@ -16,7 +16,6 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat import com.google.firebase.dataconnect.testutil.containsWithNonAdjacentText -import com.google.firebase.dataconnect.testutil.nextAlphanumericString import kotlin.random.Random import org.junit.Test import org.junit.runner.RunWith diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt index fa5415b004a..785f6ba593c 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/TestUtils.kt @@ -2,9 +2,6 @@ package com.google.firebase.dataconnect.testutil import com.google.common.truth.StringSubject import java.util.regex.Pattern -import kotlin.math.abs -import kotlin.math.min -import kotlin.random.Random import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CancellationException @@ -21,32 +18,6 @@ import kotlinx.coroutines.withContext fun StringSubject.containsWithNonAdjacentText(text: String) = containsMatch("(^|\\W)${Pattern.quote(text)}($|\\W)") -/** - * Generates and returns a string containing random alphanumeric characters. - * - * @param length the number of random characters to generate and include in the returned string; if - * `null`, then a length of 20 is used. - * @return a string containing the given (or default) number of random alphanumeric characters. - */ -fun Random.nextAlphanumericString(length: Int? = null): String = buildString { - var numCharactersRemaining = - if (length === null) 20 else length.also { require(it >= 0) { "invalid length: $it" } } - - while (numCharactersRemaining > 0) { - // Ignore the first character of the alphanumeric string because its distribution is not random. - val randomCharacters = abs(nextLong()).toAlphaNumericString() - val numCharactersToAppend = min(numCharactersRemaining, randomCharacters.length - 1) - append(randomCharacters, 1, numCharactersToAppend) - numCharactersRemaining -= numCharactersToAppend - } -} - -/** - * Converts this number to a base-36 string, which uses the 26 letters from the English alphabet and - * the 10 numeric digits. - */ -fun Long.toAlphaNumericString(): String = toString(36) - /** * Calls [kotlinx.coroutines.delay] in such a way that it _really_ will delay, even when called from * [kotlinx.coroutines.test.runTest], which _skips_ delays. This is achieved by switching contexts From 50c860e314a89f5f86f67bca17835d4a645e550f Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 29 Nov 2023 21:30:04 -0500 Subject: [PATCH 097/573] update protos with new package name from cl/586381894 --- .../testutil/EmulatorController.kt | 8 +- .../emulator/v1main}/emulator_service.proto | 6 +- .../dataconnect/DataConnectGrpcClient.kt | 8 +- .../dataconnect/v1main/data_service.proto | 101 +++++++++ .../firematdata/v0/data_service.proto | 205 ------------------ .../firematdata/v0/data_service_stream.proto | 42 ---- 6 files changed, 112 insertions(+), 258 deletions(-) rename firebase-dataconnect/src/androidTest/proto/{firebase/firemat/emulator/server/api => google/firebase/dataconnect/emulator/v1main}/emulator_service.proto (91%) create mode 100644 firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/data_service.proto delete mode 100644 firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service.proto delete mode 100644 firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt index f788efced06..033eb4240da 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt @@ -4,10 +4,10 @@ import android.content.res.AssetManager import com.google.firebase.Firebase import com.google.firebase.app import com.google.firebase.dataconnect.FirebaseDataConnect -import firemat.emulator.server.api.EmulatorServiceGrpcKt.EmulatorServiceCoroutineStub -import firemat.emulator.server.api.file -import firemat.emulator.server.api.setupSchemaRequest -import firemat.emulator.server.api.source +import google.firebase.dataconnect.emulator.v1main.EmulatorServiceGrpcKt.EmulatorServiceCoroutineStub +import google.firebase.dataconnect.emulator.v1main.file +import google.firebase.dataconnect.emulator.v1main.setupSchemaRequest +import google.firebase.dataconnect.emulator.v1main.source import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder import java.io.InputStreamReader diff --git a/firebase-dataconnect/src/androidTest/proto/firebase/firemat/emulator/server/api/emulator_service.proto b/firebase-dataconnect/src/androidTest/proto/google/firebase/dataconnect/emulator/v1main/emulator_service.proto similarity index 91% rename from firebase-dataconnect/src/androidTest/proto/firebase/firemat/emulator/server/api/emulator_service.proto rename to firebase-dataconnect/src/androidTest/proto/google/firebase/dataconnect/emulator/v1main/emulator_service.proto index be1df8a720a..93a2a7d0d33 100644 --- a/firebase-dataconnect/src/androidTest/proto/firebase/firemat/emulator/server/api/emulator_service.proto +++ b/firebase-dataconnect/src/androidTest/proto/google/firebase/dataconnect/emulator/v1main/emulator_service.proto @@ -4,9 +4,9 @@ syntax = "proto3"; -package firemat.emulator.server.api; +package google.firebase.dataconnect.emulator.v1main; -import "google/internal/firebase/firematdata/v0/data_service.proto"; +import "google/firebase/dataconnect/v1main/data_service.proto"; import "google/protobuf/empty.proto"; service EmulatorService { @@ -30,7 +30,7 @@ message GetCompileErrorsRequest { } message GetCompileErrorsResponse { - repeated google.internal.firebase.firemat.v0.GraphqlError errors = 1; + repeated google.firebase.dataconnect.v1main.GraphqlError errors = 1; } message SetupSchemaRequest { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 1018554464e..cb9e0a8b6d9 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -18,10 +18,10 @@ import com.google.android.gms.security.ProviderInstaller import com.google.protobuf.ListValue import com.google.protobuf.Struct import com.google.protobuf.Value -import google.internal.firebase.firemat.v0.DataServiceGrpcKt.DataServiceCoroutineStub -import google.internal.firebase.firemat.v0.DataServiceOuterClass.GraphqlError -import google.internal.firebase.firemat.v0.executeMutationRequest -import google.internal.firebase.firemat.v0.executeQueryRequest +import google.firebase.dataconnect.v1main.DataServiceGrpcKt.DataServiceCoroutineStub +import google.firebase.dataconnect.v1main.DataServiceOuterClass.GraphqlError +import google.firebase.dataconnect.v1main.executeMutationRequest +import google.firebase.dataconnect.v1main.executeQueryRequest import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder diff --git a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/data_service.proto b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/data_service.proto new file mode 100644 index 00000000000..9a4a7435c96 --- /dev/null +++ b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/data_service.proto @@ -0,0 +1,101 @@ +// Adapted from http://google3/firebase/firemat/emulator/server/api/data_service.proto;rcl=586381894 + +// API protos for the Firebase Data Connect Private API DataService. + +syntax = "proto3"; + +package google.firebase.dataconnect.v1main; + +import "google/api/annotations.proto"; +import "google/protobuf/struct.proto"; + +service DataService { + // REST API for executing a single pre-defined query. + // Use `operationSets/*/revisions/latest` to reference the most recent + // revision. + rpc ExecuteQuery(ExecuteQueryRequest) returns (ExecuteQueryResponse) { + option (google.api.http) = { + post: "/v0/{name=projects/*/locations/*/services/*/operationSets/*/revisions/*}:executeQuery" + body: "*" + }; + } + + // REST API for executing a single pre-defined mutation. + // Use `operationSets/*/revisions/latest` to reference the most recent + // revision. + rpc ExecuteMutation(ExecuteMutationRequest) + returns (ExecuteMutationResponse) { + option (google.api.http) = { + post: "/v0/{name=projects/*/locations/*/services/*/operationSets/*/revisions/*}:executeMutation" + body: "*" + }; + } +} + +message ExecuteQueryRequest { + // The resource name of the operation set revision that contains the query to + // execute, in the format: + // ``` + // projects/{project}/locations/{location}/services/{service}/operationSets/{operation_set}/revisions/{revision} + // ``` + // This is left empty when it is within StreamRequest. + string name = 1; + + // The name of the GraphQL operation. See + // https://graphql.org/learn/queries/#operation-name for more context. + string operation_name = 2; + + // Arbitrary JSON parameters (GraphQL variables) for the operation. + google.protobuf.Struct variables = 3; +} + +message ExecuteMutationRequest { + // The resource name of the operation set revision that contains the mutation + // to execute, in the format: + // ``` + // projects/{project}/locations/{location}/services/{service}/operationSets/{operation_set}/revisions/{revision} + // ``` + // This is left empty when it is within StreamRequest. + string name = 1; + + // The name of the GraphQL operation. See + // https://graphql.org/learn/queries/#operation-name for more context. + string operation_name = 2; + + // Arbitrary JSON parameters (GraphQL variables) for the operation. + google.protobuf.Struct variables = 3; +} + +message ExecuteQueryResponse { + google.protobuf.Struct data = 1; + repeated GraphqlError errors = 2; +} + +message ExecuteMutationResponse { + google.protobuf.Struct data = 1; + repeated GraphqlError errors = 2; +} + +message GraphqlError { + // Description of the error, intended for the developer. + string message = 1; + + // The location in the query where the error occurred. + repeated SourceLocation locations = 2; + + // The result field which could not be populated due to error. + google.protobuf.ListValue path = 3; + + // Additional error information in error extensions. + GraphqlErrorExtensions extensions = 6; +} + +message SourceLocation { + int32 line = 1; + int32 column = 2; +} + +// An error that occurred while executing a GraphQL query. +message GraphqlErrorExtensions { + string file = 1; +} \ No newline at end of file diff --git a/firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service.proto b/firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service.proto deleted file mode 100644 index e5c741bad80..00000000000 --- a/firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service.proto +++ /dev/null @@ -1,205 +0,0 @@ -// Adapted from http://google3/google/internal/firebase/firematdata/v0/data_service.proto;rcl=568574236 - -// API protos for the FireMAT Private API DataService. - -syntax = "proto3"; - -package google.internal.firebase.firemat.v0; - -import "google/api/annotations.proto"; -import "google/protobuf/struct.proto"; - -service DataService { - // REST API for executing a single pre-defined query. - // Use `operationSets/*/revisions/latest` to reference the most recent - // revision. - rpc ExecuteQuery(ExecuteQueryRequest) returns (ExecuteQueryResponse) { - option (google.api.http) = { - post: "/v0/{name=projects/*/locations/*/services/*/operationSets/*/revisions/*}:executeQuery" - body: "*" - }; - } - - // REST API for executing a single pre-defined mutation. - // Use `operationSets/*/revisions/latest` to reference the most recent - // revision. - rpc ExecuteMutation(ExecuteMutationRequest) - returns (ExecuteMutationResponse) { - option (google.api.http) = { - post: "/v0/{name=projects/*/locations/*/services/*/operationSets/*/revisions/*}:executeMutation" - body: "*" - }; - } - - // Admin access for executing GQL queries and mutations. - rpc ExecuteGraphql(GraphqlRequest) returns (GraphqlResult) { - option (google.api.http) = { - post: "/v0/{name=projects/*/locations/*/services/*}:executeGraphql" - body: "*" - }; - } - - // Admin access for executing GQL queries. - rpc ExecuteGraphqlRead(GraphqlRequest) returns (GraphqlResult) { - option (google.api.http) = { - post: "/v0/{name=projects/*/locations/*/services/*}:executeGraphqlRead" - body: "*" - }; - } -} - -message ExecuteQueryRequest { - // The resource name of the operation set revision that contains the query to - // execute, in the format: - // ``` - // projects/{project}/locations/{location}/services/{service}/operationSets/{operation_set}/revisions/{revision} - // ``` - // This is left empty when it is within StreamRequest. - string name = 1; - - // The name of the GraphQL operation. See - // https://graphql.org/learn/queries/#operation-name for more context. - string operation_name = 2; - - // Arbitrary JSON parameters (GraphQL variables) for the operation. - google.protobuf.Struct variables = 3; - - // The raw SQL to execute, along with necessary configuration for connecting - // to the Cloud SQL instance. This option is only intended for the private - // API, for testing purposes, and will be removed for launch. If this is - // specified, the resource name in the field `revision` is ignored. - SqlStatement sql_statement = 4; -} - -message ExecuteMutationRequest { - // The resource name of the operation set revision that contains the mutation - // to execute, in the format: - // ``` - // projects/{project}/locations/{location}/services/{service}/operationSets/{operation_set}/revisions/{revision} - // ``` - // This is left empty when it is within StreamRequest. - string name = 1; - - // The name of the GraphQL operation. See - // https://graphql.org/learn/queries/#operation-name for more context. - string operation_name = 2; - - // Arbitrary JSON parameters (GraphQL variables) for the operation. - google.protobuf.Struct variables = 3; - - // The raw SQL to execute, along with necessary configuration for connecting - // to the Cloud SQL instance. This option is only intended for the private - // API, for testing purposes, and will be removed for launch. If this is - // specified, the resource name in the field `revision` is ignored. - SqlStatement sql_statement = 4; -} - -message GraphqlRequest { - // The FireMAT instance name, in the format: - // ``` - // projects/{project}/locations/{location}/services/{service} - // ``` - string name = 1; - - // Values for GraphQL variables defined in the request. - google.protobuf.Struct variables = 3; - - // Auth token JSON for impersonation. Bypass `@auth` if left empty. - google.protobuf.Struct auth = 4; - - // The name of the GraphQL operation. See - // https://graphql.org/learn/queries/#operation-name for more context. - string operation_name = 5; - - // For an arbitrary query, specify `query`. See - // ​​https://graphql.org/learn/serving-over-http/. - // For a predefined query, specify `operation_set`. - oneof query_type { - string query = 2; - - // Scope to the surface storing pre-defined GQLs. - // for the latest revision - // /revisions/ otherwise - string operation_set = 6; - } -} - -message ExecuteQueryResponse { - google.protobuf.Struct data = 1; - repeated GraphqlError errors = 2; -} - -message ExecuteMutationResponse { - google.protobuf.Struct data = 1; - repeated GraphqlError errors = 2; -} - -message GraphqlResult { - google.protobuf.Struct data = 1; - repeated GraphqlError errors = 2; -} - -// Similar to cs/google3/java/com/google/cloud/boq/graphql/graphql.proto -// See https://spec.graphql.org/October2021/#sec-Errors -message GraphqlError { - // Description of the error, intended for the developer. - string message = 1; - - // The location in the query where the error occurred. - repeated SourceLocation locations = 2; - - // The result field which could not be populated due to error. - google.protobuf.ListValue path = 3; - - // Additional error information in error extensions. - GraphqlErrorExtensions extensions = 6; -} - -message SourceLocation { - int32 line = 1; - int32 column = 2; -} - -// An error that occurred while executing a GraphQL query. -message GraphqlErrorExtensions { - string file = 1; -} - -// The below three protos are intended ONLY for the private v0 API. For launch, -// configuration related to Cloud SQL connections will be stored and updated as -// part of the FireMAT control plane. - -// The SQL statement to be run against the specified Cloud SQL instance. -message SqlStatement { - CloudSqlConnection connection = 1; - string sql = 2; -} - -// Configuration information for connecting to the Cloud SQL instance. -message CloudSqlConnection { - enum DatabaseType { - DATABASE_TYPE_UNSPECIFIED = 0; - POSTGRES = 1; - MYSQL = 2; - } - DatabaseType type = 1; - - // The name of the Cloud SQL instance, in the format: - // ``` - // :: - // ``` - // For example: firebase-staging:us-central1:postgres - string instance = 2; - - // The name of the database. - string database = 3; - - // The credentials for accessing the Cloud SQL database. - CloudSqlCredential credentials = 4; -} - -// Credential info for the Cloud SQL instance. -message CloudSqlCredential { - string username = 1; - string password = 2; -} diff --git a/firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto b/firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto deleted file mode 100644 index 1f4498bc566..00000000000 --- a/firebase-dataconnect/src/main/proto/google/internal/firebase/firematdata/v0/data_service_stream.proto +++ /dev/null @@ -1,42 +0,0 @@ -// Adapted from http://google3/google/internal/firebase/firematdata/v0/data_service_stream.proto;rcl=553315079 - -// API protos for the FireMAT Private API DataService streaming API. - -syntax = "proto3"; - -package google.internal.firebase.firemat.v0; - -import "google/api/annotations.proto"; -import "google/internal/firebase/firematdata/v0/data_service.proto"; - -service DataServiceStreaming { - // Bi-directional streaming API used by client SDKs. - // Use `operationSets/*/revisions/latest` to reference the most recent - // revision. - rpc Stream(stream StreamRequest) returns (stream StreamResponse) { - option (google.api.http) = { - post: "/v0/{name=projects/*/locations/*/services/*/operationSets/*/revisions/*}:stream" - body: "*" - }; - } -} - -message StreamRequest { - // The resource name of the operation set revision to execute, in the format: - // ``` - // projects/{project}/locations/{location}/services/{service}/operationSets/{operation_set}/revisions/{revision} - // ``` - string name = 1; - - oneof operation { - ExecuteQueryRequest query = 2; - ExecuteMutationRequest mutation = 3; - } -} - -message StreamResponse { - oneof operation_response { - ExecuteQueryResponse query = 1; - ExecuteMutationResponse mutation = 2; - } -} From 0ea766f54b7353e70f57a496ba4728a8e16b3f83 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 29 Nov 2023 22:58:06 -0500 Subject: [PATCH 098/573] Move calculateSha512() to an extension function in Util.kt --- .../firebase/dataconnect/QueryManager.kt | 6 +- .../firebase/dataconnect/Sha512FromStruct.kt | 60 ------------------- .../com/google/firebase/dataconnect/Util.kt | 60 ++++++++++++++++++- 3 files changed, 60 insertions(+), 66 deletions(-) delete mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512FromStruct.kt diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index aaf740828ef..fc87d9c6e1a 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -67,7 +67,7 @@ internal class QueryManager( } } -private data class QueryStateKey(val operationName: String, val variablesSha512: String) +private data class QueryStateKey(val operationName: String, val variablesHash: String) private class QueryState( val key: QueryStateKey, @@ -255,8 +255,8 @@ private class QueryStates( // NOTE: This function MUST be called from a coroutine that has locked `mutex`. private fun acquireQueryState(ref: QueryRef, variables: V): QueryState { val variablesStruct = encodeToStruct(ref.variablesSerializer, variables) - val variablesSha512 = calculateSha512(variablesStruct).toHexString() - val key = QueryStateKey(operationName = ref.operationName, variablesSha512 = variablesSha512) + val variablesHash = variablesStruct.calculateSha512().toAlphaNumericString() + val key = QueryStateKey(operationName = ref.operationName, variablesHash = variablesHash) val queryState = queryStateByKey.getOrPut(key) { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512FromStruct.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512FromStruct.kt deleted file mode 100644 index 0a1ed8b307e..00000000000 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Sha512FromStruct.kt +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2023 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.dataconnect - -import com.google.protobuf.Struct -import com.google.protobuf.Value -import com.google.protobuf.Value.KindCase -import java.io.DataOutputStream -import java.security.DigestOutputStream -import java.security.MessageDigest - -fun calculateSha512(struct: Struct): ByteArray = - calculateSha512(Value.newBuilder().setStructValue(struct).build()) - -fun calculateSha512(value: Value): ByteArray { - val digest = MessageDigest.getInstance("SHA-512") - val out = DataOutputStream(DigestOutputStream(NullOutputStream, digest)) - - val calculateDigest = - DeepRecursiveFunction { - val kind = it.kindCase - out.writeInt(kind.ordinal) - - when (kind) { - KindCase.NULL_VALUE -> { - /* nothing to write for null */ - } - KindCase.BOOL_VALUE -> out.writeBoolean(it.boolValue) - KindCase.NUMBER_VALUE -> out.writeDouble(it.numberValue) - KindCase.STRING_VALUE -> out.writeUTF(it.stringValue) - KindCase.LIST_VALUE -> - it.listValue.valuesList.forEachIndexed { index, elementValue -> - out.writeInt(index) - callRecursive(elementValue) - } - KindCase.STRUCT_VALUE -> - it.structValue.fieldsMap.entries - .sortedBy { (key, _) -> key } - .forEach { (key, elementValue) -> - out.writeUTF(key) - callRecursive(elementValue) - } - else -> throw IllegalArgumentException("unsupported kind: $kind") - } - } - - calculateDigest(value) - return digest.digest() -} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 26554e9f877..12c28458e8a 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -13,13 +13,16 @@ // limitations under the License. package com.google.firebase.dataconnect +import com.google.protobuf.Struct +import com.google.protobuf.Value +import com.google.protobuf.Value.KindCase +import java.io.DataOutputStream import java.io.OutputStream +import java.security.DigestOutputStream +import java.security.MessageDigest import kotlin.math.abs import kotlin.random.Random -internal fun ByteArray.toHexString(): String = - joinToString(separator = "") { it.toUByte().toInt().toString(16).padStart(2, '0') } - internal object NullOutputStream : OutputStream() { override fun write(b: Int) {} override fun write(b: ByteArray?) {} @@ -56,3 +59,54 @@ internal fun Random.nextAlphanumericString(length: Int? = null): String = buildS * the 10 numeric digits. */ internal fun Long.toAlphaNumericString(): String = toString(36) + +/** + * Converts this byte array to a base-36 string, which uses the 26 letters from the English alphabet + * and the 10 numeric digits. + */ +internal fun ByteArray.toAlphaNumericString(): String = + joinToString(separator = "") { it.toUByte().toInt().toString(36).padStart(2, '0') } + +/** Calculates a SHA-512 digest of a [Struct]. */ +internal fun Struct.calculateSha512(): ByteArray = + Value.newBuilder().setStructValue(this).build().calculateSha512() + +/** Calculates a SHA-512 digest of a [Value]. */ +internal fun Value.calculateSha512(): ByteArray { + val digest = MessageDigest.getInstance("SHA-512") + val out = DataOutputStream(DigestOutputStream(NullOutputStream, digest)) + + val calculateDigest = + DeepRecursiveFunction { + val kind = it.kindCase + out.writeInt(kind.ordinal) + + when (kind) { + KindCase.NULL_VALUE -> { + /* nothing to write for null */ + } + KindCase.BOOL_VALUE -> out.writeBoolean(it.boolValue) + KindCase.NUMBER_VALUE -> out.writeDouble(it.numberValue) + KindCase.STRING_VALUE -> out.writeUTF(it.stringValue) + KindCase.LIST_VALUE -> + it.listValue.valuesList.forEachIndexed { index, elementValue -> + out.writeInt(index) + callRecursive(elementValue) + } + KindCase.STRUCT_VALUE -> + it.structValue.fieldsMap.entries + .sortedBy { (key, _) -> key } + .forEach { (key, elementValue) -> + out.writeUTF(key) + callRecursive(elementValue) + } + else -> throw IllegalArgumentException("unsupported kind: $kind") + } + + out.writeInt(kind.ordinal) + } + + calculateDigest(this) + + return digest.digest() +} From 85e134f2a61d29e08ab126f805c32dc27f886305 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 29 Nov 2023 23:28:55 -0500 Subject: [PATCH 099/573] blah --- .../dataconnect/DataConnectGrpcClient.kt | 12 +-- .../dataconnect/FirebaseDataConnect.kt | 2 +- .../firebase/dataconnect/QueryManager.kt | 2 +- .../com/google/firebase/dataconnect/Util.kt | 101 ++++++++++++++++++ 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index cb9e0a8b6d9..70aeba731c6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -131,14 +131,14 @@ internal class DataConnectGrpcClient( this.variables = variables } - logger.debug { "executeQuery() requestId=$requestId sending: $request" } + logger.debug { "executeQuery() [rid=$requestId] sending " + "ExecuteQueryRequest: ${request.toCompactString()}" } val response = mutex .withLock { grpcStub } .runCatching { executeQuery(request) } - .onFailure { logger.warn(it) { "executeQuery() requestId=$requestId grpc call FAILED" } } + .onFailure { logger.warn(it) { "executeQuery() [rid=$requestId] grpc call FAILED with ${it::class.qualifiedName}" } } .getOrThrow() - logger.debug { "executeQuery() requestId=$requestId got response: $response" } + logger.debug { "executeQuery() [rid=$requestId] received: " + "${response::class.simpleName} ${response.toCompactString()}" } return OperationResult( data = if (response.hasData()) response.data else null, @@ -157,14 +157,14 @@ internal class DataConnectGrpcClient( this.variables = variables } - logger.debug { "executeMutation() requestId=$requestId sending: $request" } + logger.debug { "executeMutation() [rid=$requestId] sending " + "${request::class.simpleName}: ${request.toCompactString()}" } val response = mutex .withLock { grpcStub } .runCatching { executeMutation(request) } - .onFailure { logger.warn(it) { "executeMutation() requestId=$requestId grpc call FAILED" } } + .onFailure { logger.warn(it) { "executeMutation() [rid=$requestId] grpc call FAILED with ${it::class.qualifiedName}" } } .getOrThrow() - logger.debug { "executeMutation() requestId=$requestId got response: $response" } + logger.debug { "executeMutation() [rid=$requestId] received: " + "${response::class.simpleName} ${response.toCompactString()}" } return OperationResult( data = if (response.hasData()) response.data else null, diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index bdb813ac305..b026b4cfc16 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -132,7 +132,7 @@ internal constructor( ) .runCatching { deserialize(ref.dataDeserializer) } .onFailure { - logger.warn(it) { "executeMutation() requestId=$requestId decoding response data failed" } + logger.warn(it) { "executeMutation() [rid=$requestId] decoding response data failed: $it" } } .getOrThrow() .toDataConnectResult(variables) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index fc87d9c6e1a..f7127b961cf 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -215,7 +215,7 @@ private class QueryState( }, onFailure = { deserializeException -> logger.warn(deserializeException) { - "executeQuery() requestId=$requestId decoding response data failed" + "executeQuery() [rid=$requestId] " + "decoding response data failed: $deserializeException" } _exceptionFlow.emit(deserializeException) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 12c28458e8a..fe49dd670b7 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -16,6 +16,17 @@ package com.google.firebase.dataconnect import com.google.protobuf.Struct import com.google.protobuf.Value import com.google.protobuf.Value.KindCase +import com.google.protobuf.listValue +import com.google.protobuf.struct +import com.google.protobuf.value +import google.firebase.dataconnect.v1main.DataServiceOuterClass +import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteMutationRequest +import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteMutationResponse +import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteQueryRequest +import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteQueryResponse +import google.firebase.dataconnect.v1main.DataServiceOuterClass.GraphqlError +import java.io.BufferedWriter +import java.io.CharArrayWriter import java.io.DataOutputStream import java.io.OutputStream import java.security.DigestOutputStream @@ -110,3 +121,93 @@ internal fun Value.calculateSha512(): ByteArray { return digest.digest() } + +/** Generates and returns a string similar to [Struct.toString] but more compact. */ +internal fun Struct.toCompactString(): String = + Value.newBuilder().setStructValue(this).build().toCompactString() + +/** Generates and returns a string similar to [Value.toString] but more compact. */ +internal fun Value.toCompactString(): String { + val charArrayWriter = CharArrayWriter() + val out = BufferedWriter(charArrayWriter) + var indent = 0 + + fun BufferedWriter.writeIndent() { + repeat(indent * 2) { write(" ") } + } + + val calculateCompactString = + DeepRecursiveFunction { + when (val kind = it.kindCase) { + KindCase.NULL_VALUE -> out.write("null") + KindCase.BOOL_VALUE -> out.write(if (it.boolValue) "true" else "false") + KindCase.NUMBER_VALUE -> out.write(it.numberValue.toString()) + KindCase.STRING_VALUE -> out.write("\"${it.stringValue}\"") + KindCase.LIST_VALUE -> { + out.write("[") + indent++ + it.listValue.valuesList.forEach { listElementValue -> + out.newLine() + out.writeIndent() + callRecursive(listElementValue) + } + indent-- + out.newLine() + out.writeIndent() + out.write("]") + } + KindCase.STRUCT_VALUE -> { + out.write("{") + indent++ + it.structValue.fieldsMap.entries + .sortedBy { (key, _) -> key } + .forEach { (structElementKey, structElementValue) -> + out.newLine() + out.writeIndent() + out.write("$structElementKey: ") + callRecursive(structElementValue) + } + indent-- + out.newLine() + out.writeIndent() + out.write("}") + } + else -> throw IllegalArgumentException("unsupported kind: $kind") + } + } + + calculateCompactString(this) + + out.close() + return charArrayWriter.toString() +} + +internal fun ExecuteQueryRequest.toCompactString(): String = struct { + fields.put("name", value { stringValue = name }) + fields.put("operationName", value { stringValue = operationName }) + fields.put("variables", value { structValue = variables }) +}.toCompactString() + +internal fun ExecuteQueryResponse.toCompactString(): String = struct { + fields.put("data", value { structValue = data }) + fields.put("errors", value { listValue = listValue { + errorsList.forEach { values.add(value { + stringValue = it.toDataConnectError().toString() + }) } + } }) +}.toCompactString() + +internal fun ExecuteMutationRequest.toCompactString(): String = struct { + fields.put("name", value { stringValue = name }) + fields.put("operationName", value { stringValue = operationName }) + fields.put("variables", value { structValue = variables }) +}.toCompactString() + +internal fun ExecuteMutationResponse.toCompactString(): String = struct { + fields.put("data", value { structValue = data }) + fields.put("errors", value { listValue = listValue { + errorsList.forEach { values.add(value { + stringValue = it.toDataConnectError().toString() + }) } + } }) +}.toCompactString() \ No newline at end of file From 7599ef34de03b8e7508956620c82f7fdada53004 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 30 Nov 2023 00:23:16 -0500 Subject: [PATCH 100/573] Implemented toCompactString() --- .../dataconnect/DataConnectGrpcClient.kt | 32 ++++++-- .../firebase/dataconnect/QueryManager.kt | 3 +- .../com/google/firebase/dataconnect/Util.kt | 78 +++++++++++-------- 3 files changed, 75 insertions(+), 38 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 70aeba731c6..0759b29ed0a 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -131,14 +131,24 @@ internal class DataConnectGrpcClient( this.variables = variables } - logger.debug { "executeQuery() [rid=$requestId] sending " + "ExecuteQueryRequest: ${request.toCompactString()}" } + logger.debug { + "executeQuery() [rid=$requestId] sending " + + "ExecuteQueryRequest: ${request.toCompactString()}" + } val response = mutex .withLock { grpcStub } .runCatching { executeQuery(request) } - .onFailure { logger.warn(it) { "executeQuery() [rid=$requestId] grpc call FAILED with ${it::class.qualifiedName}" } } + .onFailure { + logger.warn(it) { + "executeQuery() [rid=$requestId] grpc call FAILED with ${it::class.qualifiedName}" + } + } .getOrThrow() - logger.debug { "executeQuery() [rid=$requestId] received: " + "${response::class.simpleName} ${response.toCompactString()}" } + logger.debug { + "executeQuery() [rid=$requestId] received: " + + "ExecuteQueryResponse ${response.toCompactString()}" + } return OperationResult( data = if (response.hasData()) response.data else null, @@ -157,14 +167,24 @@ internal class DataConnectGrpcClient( this.variables = variables } - logger.debug { "executeMutation() [rid=$requestId] sending " + "${request::class.simpleName}: ${request.toCompactString()}" } + logger.debug { + "executeMutation() [rid=$requestId] sending " + + "ExecuteMutationRequest: ${request.toCompactString()}" + } val response = mutex .withLock { grpcStub } .runCatching { executeMutation(request) } - .onFailure { logger.warn(it) { "executeMutation() [rid=$requestId] grpc call FAILED with ${it::class.qualifiedName}" } } + .onFailure { + logger.warn(it) { + "executeMutation() [rid=$requestId] grpc call FAILED with ${it::class.qualifiedName}" + } + } .getOrThrow() - logger.debug { "executeMutation() [rid=$requestId] received: " + "${response::class.simpleName} ${response.toCompactString()}" } + logger.debug { + "executeMutation() [rid=$requestId] received: " + + "ExecuteMutationResponse ${response.toCompactString()}" + } return OperationResult( data = if (response.hasData()) response.data else null, diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index f7127b961cf..4602e7b06d2 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -215,7 +215,8 @@ private class QueryState( }, onFailure = { deserializeException -> logger.warn(deserializeException) { - "executeQuery() [rid=$requestId] " + "decoding response data failed: $deserializeException" + "executeQuery() [rid=$requestId] " + + "decoding response data failed: $deserializeException" } _exceptionFlow.emit(deserializeException) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index fe49dd670b7..dac36384aa8 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -19,12 +19,10 @@ import com.google.protobuf.Value.KindCase import com.google.protobuf.listValue import com.google.protobuf.struct import com.google.protobuf.value -import google.firebase.dataconnect.v1main.DataServiceOuterClass import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteMutationRequest import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteMutationResponse import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteQueryRequest import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteQueryResponse -import google.firebase.dataconnect.v1main.DataServiceOuterClass.GraphqlError import java.io.BufferedWriter import java.io.CharArrayWriter import java.io.DataOutputStream @@ -182,32 +180,50 @@ internal fun Value.toCompactString(): String { return charArrayWriter.toString() } -internal fun ExecuteQueryRequest.toCompactString(): String = struct { - fields.put("name", value { stringValue = name }) - fields.put("operationName", value { stringValue = operationName }) - fields.put("variables", value { structValue = variables }) -}.toCompactString() - -internal fun ExecuteQueryResponse.toCompactString(): String = struct { - fields.put("data", value { structValue = data }) - fields.put("errors", value { listValue = listValue { - errorsList.forEach { values.add(value { - stringValue = it.toDataConnectError().toString() - }) } - } }) -}.toCompactString() - -internal fun ExecuteMutationRequest.toCompactString(): String = struct { - fields.put("name", value { stringValue = name }) - fields.put("operationName", value { stringValue = operationName }) - fields.put("variables", value { structValue = variables }) -}.toCompactString() - -internal fun ExecuteMutationResponse.toCompactString(): String = struct { - fields.put("data", value { structValue = data }) - fields.put("errors", value { listValue = listValue { - errorsList.forEach { values.add(value { - stringValue = it.toDataConnectError().toString() - }) } - } }) -}.toCompactString() \ No newline at end of file +internal fun ExecuteQueryRequest.toCompactString(): String = + struct { + fields.put("name", value { stringValue = name }) + fields.put("operationName", value { stringValue = operationName }) + if (hasVariables()) fields.put("variables", value { structValue = variables }) + } + .toCompactString() + +internal fun ExecuteQueryResponse.toCompactString(): String = + struct { + if (hasData()) fields.put("data", value { structValue = data }) + fields.put( + "errors", + value { + listValue = listValue { + errorsList.forEach { + values.add(value { stringValue = it.toDataConnectError().toString() }) + } + } + } + ) + } + .toCompactString() + +internal fun ExecuteMutationRequest.toCompactString(): String = + struct { + fields.put("name", value { stringValue = name }) + fields.put("operationName", value { stringValue = operationName }) + if (hasVariables()) fields.put("variables", value { structValue = variables }) + } + .toCompactString() + +internal fun ExecuteMutationResponse.toCompactString(): String = + struct { + if (hasData()) fields.put("data", value { structValue = data }) + fields.put( + "errors", + value { + listValue = listValue { + errorsList.forEach { + values.add(value { stringValue = it.toDataConnectError().toString() }) + } + } + } + ) + } + .toCompactString() From 872ec7c443e3e7308807720a95386264dda851ed Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 30 Nov 2023 13:43:16 -0500 Subject: [PATCH 101/573] Add a more convenient struct builder --- .../dataconnect/ProtoStructEncoder.kt | 62 ++-- .../google/firebase/dataconnect/ProtoUtil.kt | 304 ++++++++++++++++++ .../com/google/firebase/dataconnect/Util.kt | 198 ++---------- .../dataconnect/ProtoStructDecoderTest.kt | 9 +- .../dataconnect/ProtoStructEncoderTest.kt | 117 ++----- .../google/firebase/dataconnect/UtilTest.kt | 144 +++++++-- 6 files changed, 507 insertions(+), 327 deletions(-) create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt index 830102eabda..3d1be8ab2ff 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt @@ -3,7 +3,6 @@ package com.google.firebase.dataconnect import com.google.protobuf.ListValue -import com.google.protobuf.NullValue import com.google.protobuf.Struct import com.google.protobuf.Value import com.google.protobuf.Value.KindCase @@ -35,29 +34,6 @@ fun encodeToStruct(serializer: SerializationStrategy, value: T): Struct { return valueProto.structValue } -private fun Boolean.toProtoValue(): Value = Value.newBuilder().setBoolValue(this).build() - -private fun Byte.toProtoValue(): Value = toInt().toProtoValue() - -private fun Char.toProtoValue(): Value = code.toProtoValue() - -private fun Double.toProtoValue(): Value = Value.newBuilder().setNumberValue(this).build() - -private fun Float.toProtoValue(): Value = toDouble().toProtoValue() - -private fun Int.toProtoValue(): Value = toDouble().toProtoValue() - -private fun Long.toProtoValue(): Value = toString().toProtoValue() - -private fun Short.toProtoValue(): Value = toInt().toProtoValue() - -private fun String.toProtoValue(): Value = Value.newBuilder().setStringValue(this).build() - -private val nullProtoValue: Value - get() { - return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build() - } - private class ProtoValueEncoder(private val path: String?, private val onValue: (Value) -> Unit) : Encoder { @@ -73,37 +49,37 @@ private class ProtoValueEncoder(private val path: String?, private val onValue: } override fun encodeBoolean(value: Boolean) { - onValue(value.toProtoValue()) + onValue(value.toValueProto()) } override fun encodeByte(value: Byte) { - onValue(value.toProtoValue()) + onValue(value.toValueProto()) } override fun encodeChar(value: Char) { - onValue(value.toProtoValue()) + onValue(value.toValueProto()) } override fun encodeDouble(value: Double) { - onValue(value.toProtoValue()) + onValue(value.toValueProto()) } override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { - onValue(index.toProtoValue()) + onValue(index.toValueProto()) } override fun encodeFloat(value: Float) { - onValue(value.toProtoValue()) + onValue(value.toValueProto()) } override fun encodeInline(descriptor: SerialDescriptor) = this override fun encodeInt(value: Int) { - onValue(value.toProtoValue()) + onValue(value.toValueProto()) } override fun encodeLong(value: Long) { - onValue(value.toProtoValue()) + onValue(value.toValueProto()) } @ExperimentalSerializationApi @@ -117,11 +93,11 @@ private class ProtoValueEncoder(private val path: String?, private val onValue: } override fun encodeShort(value: Short) { - onValue(value.toProtoValue()) + onValue(value.toValueProto()) } override fun encodeString(value: String) { - onValue(value.toProtoValue()) + onValue(value.toValueProto()) } } @@ -142,23 +118,23 @@ private abstract class ProtoCompositeValueEncoder( protected abstract fun formattedKeyForElementPath(key: K): String override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { - putValue(descriptor, index, value.toProtoValue()) + putValue(descriptor, index, value.toValueProto()) } override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) { - putValue(descriptor, index, value.toProtoValue()) + putValue(descriptor, index, value.toValueProto()) } override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) { - putValue(descriptor, index, value.toProtoValue()) + putValue(descriptor, index, value.toValueProto()) } override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) { - putValue(descriptor, index, value.toProtoValue()) + putValue(descriptor, index, value.toValueProto()) } override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) { - putValue(descriptor, index, value.toProtoValue()) + putValue(descriptor, index, value.toValueProto()) } override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder { @@ -166,19 +142,19 @@ private abstract class ProtoCompositeValueEncoder( } override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) { - putValue(descriptor, index, value.toProtoValue()) + putValue(descriptor, index, value.toValueProto()) } override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) { - putValue(descriptor, index, value.toProtoValue()) + putValue(descriptor, index, value.toValueProto()) } override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) { - putValue(descriptor, index, value.toProtoValue()) + putValue(descriptor, index, value.toValueProto()) } override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) { - putValue(descriptor, index, value.toProtoValue()) + putValue(descriptor, index, value.toValueProto()) } @ExperimentalSerializationApi diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt new file mode 100644 index 00000000000..89f0ef51140 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt @@ -0,0 +1,304 @@ +// Copyright 2023 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.dataconnect + +import com.google.protobuf.ListValue +import com.google.protobuf.NullValue +import com.google.protobuf.Struct +import com.google.protobuf.Value +import com.google.protobuf.Value.KindCase +import com.google.protobuf.listValueOrNull +import com.google.protobuf.structValueOrNull +import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteMutationRequest +import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteMutationResponse +import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteQueryRequest +import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteQueryResponse +import java.io.BufferedWriter +import java.io.CharArrayWriter +import java.io.DataOutputStream +import java.security.DigestOutputStream +import java.security.MessageDigest + +/** Calculates a SHA-512 digest of a [Struct]. */ +internal fun Struct.calculateSha512(): ByteArray = + Value.newBuilder().setStructValue(this).build().calculateSha512() + +/** Calculates a SHA-512 digest of a [Value]. */ +internal fun Value.calculateSha512(): ByteArray { + val digest = MessageDigest.getInstance("SHA-512") + val out = DataOutputStream(DigestOutputStream(NullOutputStream, digest)) + + val calculateDigest = + DeepRecursiveFunction { + val kind = it.kindCase + out.writeInt(kind.ordinal) + + when (kind) { + KindCase.NULL_VALUE -> { + /* nothing to write for null */ + } + KindCase.BOOL_VALUE -> out.writeBoolean(it.boolValue) + KindCase.NUMBER_VALUE -> out.writeDouble(it.numberValue) + KindCase.STRING_VALUE -> out.writeUTF(it.stringValue) + KindCase.LIST_VALUE -> + it.listValue.valuesList.forEachIndexed { index, elementValue -> + out.writeInt(index) + callRecursive(elementValue) + } + KindCase.STRUCT_VALUE -> + it.structValue.fieldsMap.entries + .sortedBy { (key, _) -> key } + .forEach { (key, elementValue) -> + out.writeUTF(key) + callRecursive(elementValue) + } + else -> throw IllegalArgumentException("unsupported kind: $kind") + } + + out.writeInt(kind.ordinal) + } + + calculateDigest(this) + + return digest.digest() +} + +internal fun Boolean.toValueProto(): Value = Value.newBuilder().setBoolValue(this).build() + +internal fun Byte.toValueProto(): Value = toInt().toValueProto() + +internal fun Char.toValueProto(): Value = code.toValueProto() + +internal fun Double.toValueProto(): Value = Value.newBuilder().setNumberValue(this).build() + +internal fun Float.toValueProto(): Value = toDouble().toValueProto() + +internal fun Int.toValueProto(): Value = toDouble().toValueProto() + +internal fun Long.toValueProto(): Value = toString().toValueProto() + +internal fun Short.toValueProto(): Value = toInt().toValueProto() + +internal fun String.toValueProto(): Value = Value.newBuilder().setStringValue(this).build() + +internal fun ListValue.toValueProto(): Value = Value.newBuilder().setListValue(this).build() + +internal fun Struct.toValueProto(): Value = Value.newBuilder().setStructValue(this).build() + +internal val nullProtoValue: Value + get() { + return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build() + } + +@DslMarker internal annotation class StructProtoBuilderDslMarker + +@StructProtoBuilderDslMarker +internal class StructProtoBuilder(struct: Struct? = null) { + private val builder = struct?.toBuilder() ?: Struct.newBuilder() + + fun build(): Struct = builder.build() + + fun clear() { + builder.clearFields() + } + + fun remove(key: String) { + builder.removeFields(key) + } + + fun put(key: String, value: Double?) { + builder.putFields(key, value?.toValueProto() ?: nullProtoValue) + } + + fun put(key: String, value: Int?) { + builder.putFields(key, value?.toValueProto() ?: nullProtoValue) + } + + fun put(key: String, value: Boolean?) { + builder.putFields(key, value?.toValueProto() ?: nullProtoValue) + } + + fun put(key: String, value: String?) { + builder.putFields(key, value?.toValueProto() ?: nullProtoValue) + } + + fun put(key: String, value: ListValue?) { + builder.putFields(key, value?.toValueProto() ?: nullProtoValue) + } + + fun putList(key: String, block: ListValueProtoBuilder.() -> Unit) { + val initialValue = builder.getFieldsOrDefault(key, Value.getDefaultInstance()).listValueOrNull + builder.putFields(key, ListValueProtoBuilder(initialValue).apply(block).build().toValueProto()) + } + + fun put(key: String, value: Struct?) { + builder.putFields(key, value?.toValueProto() ?: nullProtoValue) + } + + fun putStruct(key: String, block: StructProtoBuilder.() -> Unit) { + val initialValue = builder.getFieldsOrDefault(key, Value.getDefaultInstance()).structValueOrNull + builder.putFields(key, StructProtoBuilder(initialValue).apply(block).build().toValueProto()) + } + + fun putNull(key: String) { + builder.putFields(key, nullProtoValue) + } +} + +@StructProtoBuilderDslMarker +internal class ListValueProtoBuilder(listValue: ListValue? = null) { + private val builder = listValue?.toBuilder() ?: ListValue.newBuilder() + + fun build(): ListValue = builder.build() + + fun clear() { + builder.clearValues() + } + + fun removeAt(index: Int) { + builder.removeValues(index) + } + + fun add(value: Double?) { + builder.addValues(value?.toValueProto() ?: nullProtoValue) + } + + fun add(value: Int?) { + builder.addValues(value?.toValueProto() ?: nullProtoValue) + } + + fun add(value: Boolean?) { + builder.addValues(value?.toValueProto() ?: nullProtoValue) + } + + fun add(value: String?) { + builder.addValues(value?.toValueProto() ?: nullProtoValue) + } + + fun add(value: ListValue?) { + builder.addValues(value?.toValueProto() ?: nullProtoValue) + } + + fun addList(block: ListValueProtoBuilder.() -> Unit) { + builder.addValues(ListValueProtoBuilder().apply(block).build().toValueProto()) + } + + fun add(value: Struct?) { + builder.addValues(value?.toValueProto() ?: nullProtoValue) + } + + fun addStruct(block: StructProtoBuilder.() -> Unit) { + builder.addValues(StructProtoBuilder().apply(block).build().toValueProto()) + } + + fun addNull() { + builder.addValues(nullProtoValue) + } +} + +/** A more convenient builder for [Struct] than [com.google.protobuf.struct]. */ +internal fun buildStructProto( + initialValues: Struct? = null, + block: StructProtoBuilder.() -> Unit +): Struct = StructProtoBuilder(initialValues).apply(block).build() + +/** Generates and returns a string similar to [Struct.toString] but more compact. */ +internal fun Struct.toCompactString(): String = + Value.newBuilder().setStructValue(this).build().toCompactString() + +/** Generates and returns a string similar to [Value.toString] but more compact. */ +internal fun Value.toCompactString(): String { + val charArrayWriter = CharArrayWriter() + val out = BufferedWriter(charArrayWriter) + var indent = 0 + + fun BufferedWriter.writeIndent() { + repeat(indent * 2) { write(" ") } + } + + val calculateCompactString = + DeepRecursiveFunction { + when (val kind = it.kindCase) { + KindCase.NULL_VALUE -> out.write("null") + KindCase.BOOL_VALUE -> out.write(if (it.boolValue) "true" else "false") + KindCase.NUMBER_VALUE -> out.write(it.numberValue.toString()) + KindCase.STRING_VALUE -> out.write("\"${it.stringValue}\"") + KindCase.LIST_VALUE -> { + out.write("[") + indent++ + it.listValue.valuesList.forEach { listElementValue -> + out.newLine() + out.writeIndent() + callRecursive(listElementValue) + } + indent-- + out.newLine() + out.writeIndent() + out.write("]") + } + KindCase.STRUCT_VALUE -> { + out.write("{") + indent++ + it.structValue.fieldsMap.entries + .sortedBy { (key, _) -> key } + .forEach { (structElementKey, structElementValue) -> + out.newLine() + out.writeIndent() + out.write("$structElementKey: ") + callRecursive(structElementValue) + } + indent-- + out.newLine() + out.writeIndent() + out.write("}") + } + else -> throw IllegalArgumentException("unsupported kind: $kind") + } + } + + calculateCompactString(this) + + out.close() + return charArrayWriter.toString() +} + +internal fun ExecuteQueryRequest.toCompactString(): String = + buildStructProto { + put("name", name) + put("operationName", operationName) + if (hasVariables()) put("variables", variables) + } + .toCompactString() + +internal fun ExecuteQueryResponse.toCompactString(): String = + buildStructProto { + if (hasData()) put("data", data) + putList("errors") { errorsList.forEach { add(it.toDataConnectError().toString()) } } + } + .toCompactString() + +internal fun ExecuteMutationRequest.toCompactString(): String = + buildStructProto { + put("name", name) + put("operationName", operationName) + if (hasVariables()) put("variables", variables) + } + .toCompactString() + +internal fun ExecuteMutationResponse.toCompactString(): String = + buildStructProto { + if (hasData()) put("data", data) + putList("errors") { errorsList.forEach { add(it.toDataConnectError().toString()) } } + } + .toCompactString() diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index dac36384aa8..282525fb5b3 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -13,22 +13,7 @@ // limitations under the License. package com.google.firebase.dataconnect -import com.google.protobuf.Struct -import com.google.protobuf.Value -import com.google.protobuf.Value.KindCase -import com.google.protobuf.listValue -import com.google.protobuf.struct -import com.google.protobuf.value -import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteMutationRequest -import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteMutationResponse -import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteQueryRequest -import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteQueryResponse -import java.io.BufferedWriter -import java.io.CharArrayWriter -import java.io.DataOutputStream import java.io.OutputStream -import java.security.DigestOutputStream -import java.security.MessageDigest import kotlin.math.abs import kotlin.random.Random @@ -63,6 +48,11 @@ internal fun Random.nextAlphanumericString(length: Int? = null): String = buildS } } +// NOTE: `ALPHANUMERIC_ALPHABET` MUST have a length of 32 (since 2^5=32). This allows encoding 5 +// bits as a single digit from this alphabet. Note that some numbers and letters were removed, +// especially those that can look similar in different fonts, like '1', 'l', and 'i'. +private const val ALPHANUMERIC_ALPHABET = "23456789abcdefghjkmnopqrstuvwxyz" + /** * Converts this number to a base-36 string, which uses the 26 letters from the English alphabet and * the 10 numeric digits. @@ -73,157 +63,39 @@ internal fun Long.toAlphaNumericString(): String = toString(36) * Converts this byte array to a base-36 string, which uses the 26 letters from the English alphabet * and the 10 numeric digits. */ -internal fun ByteArray.toAlphaNumericString(): String = - joinToString(separator = "") { it.toUByte().toInt().toString(36).padStart(2, '0') } - -/** Calculates a SHA-512 digest of a [Struct]. */ -internal fun Struct.calculateSha512(): ByteArray = - Value.newBuilder().setStructValue(this).build().calculateSha512() - -/** Calculates a SHA-512 digest of a [Value]. */ -internal fun Value.calculateSha512(): ByteArray { - val digest = MessageDigest.getInstance("SHA-512") - val out = DataOutputStream(DigestOutputStream(NullOutputStream, digest)) - - val calculateDigest = - DeepRecursiveFunction { - val kind = it.kindCase - out.writeInt(kind.ordinal) - - when (kind) { - KindCase.NULL_VALUE -> { - /* nothing to write for null */ - } - KindCase.BOOL_VALUE -> out.writeBoolean(it.boolValue) - KindCase.NUMBER_VALUE -> out.writeDouble(it.numberValue) - KindCase.STRING_VALUE -> out.writeUTF(it.stringValue) - KindCase.LIST_VALUE -> - it.listValue.valuesList.forEachIndexed { index, elementValue -> - out.writeInt(index) - callRecursive(elementValue) +internal fun ByteArray.toAlphaNumericString(): String = buildString { + val numBits = size * 8 + for (bitIndex in 0 until numBits step 5) { + val byteIndex = bitIndex.div(8) + val bitOffset = bitIndex.rem(8) + val b = this@toAlphaNumericString[byteIndex].toUByte().toInt() + + val intValue = + if (bitOffset <= 3) { + b shr (3 - bitOffset) + } else { + val upperBits = + when (bitOffset) { + 4 -> b and 0x0f + 5 -> b and 0x07 + 6 -> b and 0x03 + 7 -> b and 0x01 + else -> error("internal error: invalid bitOffset: $bitOffset") } - KindCase.STRUCT_VALUE -> - it.structValue.fieldsMap.entries - .sortedBy { (key, _) -> key } - .forEach { (key, elementValue) -> - out.writeUTF(key) - callRecursive(elementValue) - } - else -> throw IllegalArgumentException("unsupported kind: $kind") - } - - out.writeInt(kind.ordinal) - } - - calculateDigest(this) - - return digest.digest() -} - -/** Generates and returns a string similar to [Struct.toString] but more compact. */ -internal fun Struct.toCompactString(): String = - Value.newBuilder().setStructValue(this).build().toCompactString() - -/** Generates and returns a string similar to [Value.toString] but more compact. */ -internal fun Value.toCompactString(): String { - val charArrayWriter = CharArrayWriter() - val out = BufferedWriter(charArrayWriter) - var indent = 0 - - fun BufferedWriter.writeIndent() { - repeat(indent * 2) { write(" ") } - } - - val calculateCompactString = - DeepRecursiveFunction { - when (val kind = it.kindCase) { - KindCase.NULL_VALUE -> out.write("null") - KindCase.BOOL_VALUE -> out.write(if (it.boolValue) "true" else "false") - KindCase.NUMBER_VALUE -> out.write(it.numberValue.toString()) - KindCase.STRING_VALUE -> out.write("\"${it.stringValue}\"") - KindCase.LIST_VALUE -> { - out.write("[") - indent++ - it.listValue.valuesList.forEach { listElementValue -> - out.newLine() - out.writeIndent() - callRecursive(listElementValue) + if (byteIndex + 1 == size) { + upperBits + } else { + val b2 = this@toAlphaNumericString[byteIndex + 1].toUByte().toInt() + when (bitOffset) { + 4 -> ((b2 shr 7) and 0x01) or (upperBits shl 1) + 5 -> ((b2 shr 6) and 0x03) or (upperBits shl 2) + 6 -> ((b2 shr 5) and 0x07) or (upperBits shl 3) + 7 -> ((b2 shr 4) and 0x0f) or (upperBits shl 4) + else -> error("internal error: invalid bitOffset: $bitOffset") } - indent-- - out.newLine() - out.writeIndent() - out.write("]") } - KindCase.STRUCT_VALUE -> { - out.write("{") - indent++ - it.structValue.fieldsMap.entries - .sortedBy { (key, _) -> key } - .forEach { (structElementKey, structElementValue) -> - out.newLine() - out.writeIndent() - out.write("$structElementKey: ") - callRecursive(structElementValue) - } - indent-- - out.newLine() - out.writeIndent() - out.write("}") - } - else -> throw IllegalArgumentException("unsupported kind: $kind") } - } - - calculateCompactString(this) - out.close() - return charArrayWriter.toString() + append(ALPHANUMERIC_ALPHABET[intValue and 0x1f]) + } } - -internal fun ExecuteQueryRequest.toCompactString(): String = - struct { - fields.put("name", value { stringValue = name }) - fields.put("operationName", value { stringValue = operationName }) - if (hasVariables()) fields.put("variables", value { structValue = variables }) - } - .toCompactString() - -internal fun ExecuteQueryResponse.toCompactString(): String = - struct { - if (hasData()) fields.put("data", value { structValue = data }) - fields.put( - "errors", - value { - listValue = listValue { - errorsList.forEach { - values.add(value { stringValue = it.toDataConnectError().toString() }) - } - } - } - ) - } - .toCompactString() - -internal fun ExecuteMutationRequest.toCompactString(): String = - struct { - fields.put("name", value { stringValue = name }) - fields.put("operationName", value { stringValue = operationName }) - if (hasVariables()) fields.put("variables", value { structValue = variables }) - } - .toCompactString() - -internal fun ExecuteMutationResponse.toCompactString(): String = - struct { - if (hasData()) fields.put("data", value { structValue = data }) - fields.put( - "errors", - value { - listValue = listValue { - errorsList.forEach { - values.add(value { stringValue = it.toDataConnectError().toString() }) - } - } - } - ) - } - .toCompactString() diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt index 1ff9e11a968..209c1b2921f 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderTest.kt @@ -17,12 +17,9 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat -import com.google.protobuf.NullValue import com.google.protobuf.Struct import com.google.protobuf.Value import com.google.protobuf.Value.KindCase -import com.google.protobuf.struct -import com.google.protobuf.value import java.util.regex.Pattern import kotlin.reflect.KClass import kotlinx.serialization.ExperimentalSerializationApi @@ -654,11 +651,7 @@ class ProtoStructDecoderTest { @Serializable data class TestDecodeSubData2(val someValue: String) @Serializable data class TestDecodeSubData(val bbb: TestDecodeSubData2) @Serializable data class TestDecodeData(val aaa: TestDecodeSubData) - val struct = struct { - fields["aaa"] = value { - structValue = struct { fields["bbb"] = value { nullValue = NullValue.NULL_VALUE } } - } - } + val struct = buildStructProto { putStruct("aaa") { putNull("bbb") } } assertDecodeFromStructThrowsIncorrectKindCase( expectedKind = KindCase.STRUCT_VALUE, diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt index 575eb755b20..c0a62af9847 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructEncoderTest.kt @@ -18,10 +18,6 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat -import com.google.protobuf.NullValue -import com.google.protobuf.listValue -import com.google.protobuf.struct -import com.google.protobuf.value import java.util.concurrent.atomic.AtomicLong import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable @@ -105,13 +101,13 @@ class ProtoStructEncoderTest { assertThat(encodedStruct) .isEqualTo( - struct { - fields.put("iv", value { numberValue = 42.0 }) - fields.put("dv", value { numberValue = 1234.5 }) - fields.put("bvt", value { boolValue = true }) - fields.put("bvf", value { boolValue = false }) - fields.put("sv", value { stringValue = "blah blah" }) - fields.put("nsvnn", value { stringValue = "I'm not null" }) + buildStructProto { + put("iv", 42.0) + put("dv", 1234.5) + put("bvt", true) + put("bvf", false) + put("sv", "blah blah") + put("nsvnn", "I'm not null") } ) } @@ -139,66 +135,31 @@ class ProtoStructEncoderTest { assertThat(encodedStruct) .isEqualTo( - struct { - fields.put( - "iv", - value { - listValue = listValue { - values.apply { - add(value { numberValue = 42.0 }) - add(value { numberValue = 43.0 }) - } - } - } - ) - fields.put( - "dv", - value { - listValue = listValue { - values.apply { - add(value { numberValue = 1234.5 }) - add(value { numberValue = 5678.9 }) - } - } - } - ) - fields.put( - "bv", - value { - listValue = listValue { - values.apply { - add(value { boolValue = true }) - add(value { boolValue = false }) - add(value { boolValue = false }) - add(value { boolValue = true }) - } - } - } - ) - fields.put( - "sv", - value { - listValue = listValue { - values.apply { - add(value { stringValue = "abcde" }) - add(value { stringValue = "fghij" }) - } - } - } - ) - fields.put( - "nsv", - value { - listValue = listValue { - values.apply { - add(value { stringValue = "klmno" }) - add(value { nullValue = NullValue.NULL_VALUE }) - add(value { stringValue = "pqrst" }) - add(value { nullValue = NullValue.NULL_VALUE }) - } - } - } - ) + buildStructProto { + putList("iv") { + add(42.0) + add(43.0) + } + putList("dv") { + add(1234.5) + add(5678.9) + } + putList("bv") { + add(true) + add(false) + add(false) + add(true) + } + putList("sv") { + add("abcde") + add("fghij") + } + putList("nsv") { + add("klmno") + addNull() + add("pqrst") + addNull() + } } ) } @@ -212,19 +173,7 @@ class ProtoStructEncoderTest { assertThat(encodedStruct) .isEqualTo( - struct { - fields.put( - "data2", - value { - structValue = struct { - fields.put( - "data3", - value { structValue = struct { fields.put("s", value { stringValue = "zzzz" }) } } - ) - } - } - ) - } + buildStructProto { putStruct("data2") { putStruct("data3") { put("s", "zzzz") } } } ) } } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/UtilTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/UtilTest.kt index 6f9013b8334..8a4351f67ff 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/UtilTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/UtilTest.kt @@ -20,70 +20,156 @@ import org.junit.Test class UtilTest { @Test - fun `ByteArray toHexString() on empty byte array`() { + fun `ByteArray toAlphaNumericString() interprets the alphabet`() { + val byteArray = + byteArrayOf( + 0, + 68, + 50, + 20, + -57, + 66, + 84, + -74, + 53, + -49, + -124, + 101, + 58, + 86, + -41, + -58, + 117, + -66, + 119, + -33 + ) + // This string is `ALPHANUMERIC_ALPHABET` in `Util.kt` + assertThat(byteArray.toAlphaNumericString()).isEqualTo("23456789abcdefghjkmnopqrstuvwxyz") + } + + @Test + fun `ByteArray toAlphaNumericString() where the final 5-bit chunk is 1 bit`() { + byteArrayOf(75, 50).let { assertThat(it.toAlphaNumericString()).isEqualTo("bet2") } + byteArrayOf(75, 51).let { assertThat(it.toAlphaNumericString()).isEqualTo("bet3") } + } + + @Test + fun `ByteArray toAlphaNumericString() where the final 5-bit chunk is 2 bits`() { + byteArrayOf(117, -40, -116, -66, -105, -61, 18, -117, -52).let { + assertThat(it.toAlphaNumericString()).isEqualTo("greathorsebarn2") + } + byteArrayOf(117, -40, -116, -66, -105, -61, 18, -117, -49).let { + assertThat(it.toAlphaNumericString()).isEqualTo("greathorsebarn5") + } + } + + @Test + fun `ByteArray toAlphaNumericString() where the final 5-bit chunk is 3 bits`() { + byteArrayOf(64).let { assertThat(it.toAlphaNumericString()).isEqualTo("a2") } + byteArrayOf(71).let { assertThat(it.toAlphaNumericString()).isEqualTo("a9") } + } + + @Test + fun `ByteArray toAlphaNumericString() where the final 5-bit chunk is 4 bits`() { + byteArrayOf(-58, 117, 48).let { assertThat(it.toAlphaNumericString()).isEqualTo("stun2") } + byteArrayOf(-58, 117, 63).let { assertThat(it.toAlphaNumericString()).isEqualTo("stunh") } + } + + @Test + fun `ByteArray toAlphaNumericString() on empty byte array`() { val emptyByteArray = byteArrayOf() - assertThat(emptyByteArray.toHexString()).isEqualTo("") + assertThat(emptyByteArray.toAlphaNumericString()).isEqualTo("") } @Test - fun `ByteArray toHexString() on byte array with 1 element of value 0`() { + fun `ByteArray toAlphaNumericString() on byte array with 1 element of value 0`() { val byteArray = byteArrayOf(0) - assertThat(byteArray.toHexString()).isEqualTo("00") + assertThat(byteArray.toAlphaNumericString()).isEqualTo("22") } @Test - fun `ByteArray toHexString() on byte array with 1 element of value 1`() { + fun `ByteArray toAlphaNumericString() on byte array with 1 element of value 1`() { val byteArray = byteArrayOf(1) - assertThat(byteArray.toHexString()).isEqualTo("01") + assertThat(byteArray.toAlphaNumericString()).isEqualTo("23") } @Test - fun `ByteArray toHexString() on byte array with 1 element of value 0xff`() { + fun `ByteArray toAlphaNumericString() on byte array with 1 element of value 0xff`() { val byteArray = byteArrayOf(0xff.toByte()) - assertThat(byteArray.toHexString()).isEqualTo("ff") + assertThat(byteArray.toAlphaNumericString()).isEqualTo("z9") } @Test - fun `ByteArray toHexString() on byte array with 1 element of value -1`() { + fun `ByteArray toAlphaNumericString() on byte array with 1 element of value -1`() { val byteArray = byteArrayOf(-1) - assertThat(byteArray.toHexString()).isEqualTo("ff") + assertThat(byteArray.toAlphaNumericString()).isEqualTo("z9") } @Test - fun `ByteArray toHexString() on byte array with 1 element of value MIN_VALUE`() { + fun `ByteArray toAlphaNumericString() on byte array with 1 element of value MIN_VALUE`() { val byteArray = byteArrayOf(Byte.MIN_VALUE) - assertThat(byteArray.toHexString()).isEqualTo("80") + assertThat(byteArray.toAlphaNumericString()).isEqualTo("j2") } @Test - fun `ByteArray toHexString() on byte array with 1 element of value MAX_VALUE`() { + fun `ByteArray toAlphaNumericString() on byte array with 1 element of value MAX_VALUE`() { val byteArray = byteArrayOf(Byte.MAX_VALUE) - assertThat(byteArray.toHexString()).isEqualTo("7f") + assertThat(byteArray.toAlphaNumericString()).isEqualTo("h9") } @Test - fun `ByteArray toHexString() on byte array containing all possible values`() { + fun `ByteArray toAlphaNumericString() on byte array containing all possible values`() { val byteArray = - buildList { + buildList { for (i in 0 until 512) { add(i.toByte()) } } .toByteArray() - assertThat(byteArray.toHexString()) + assertThat(byteArray.toAlphaNumericString()) .isEqualTo( - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2" + - "b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455" + - "565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8" + - "08182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aa" + - "abacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d" + - "5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292" + - "a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f5051525354" + - "55565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7" + - "f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9" + - "aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d" + - "4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + "222j62s62o52g42b3a7js5ag3wa346jn4jcke7ss56f3q92x5shm2ab46em4cbk972omocte7or4ye3k8atnafbq" + + "8ww5mgkv9jynwhu2a7368k47at5ojmccbf86unmhc3ap6ouocpd7gq4tdbfpsrcydxj84sn5ekmqetvaf7p8qv" + + "5fftrr2wdmgfu9cxnrh3wroyvwhpz9z263jc3sb3e8jy6an4odkm8sx5wjm8bb976pmudtk8eunggbv9ozo4ju" + + "7ax6oqnchc7bpcputdfgpysd5epnqmuvffxsr8xdrh7xruzw3jg4sh4edkq9t56wpmyetr9ezo8kudbxbpgquz" + + "efnqqvvngxxrz2w9kg9t97wvnykuhcxhqgvvrhy5sz7wzoyrvhhy9tzdxztzhyzw2242j52j4je3sa3672q52f" + + "3s9k26am4ec3c7jr52eko8sw5oh3ya336akmabb86wo4mckd7jqmwdtj86t58f3p8svnjgbu9ey5uhkza32o6j" + + "u6ap56gm4bbb7osncgbxa74omnckcpepusd7f7qr4xdthq2sd4efm8ctn9f3oqouvefpr8yw5kgbtraxdqgxw9" + + "mynvhkyrwzw2j83a9367ju5sk4eckg8av5ohm4at76womqdbh86tncftt9eynyjc5ap5ommufbxap8pcrd7fpu" + + "rv3efmqguddfprr4wvpgxwrqzdzj83sd3wbkg8sz6enmqdtn8wxnyju9bf9p8puvdxkqguvhgfvrqzw5jy7sz6" + + "wrnghu9bxdpytvhgxzsh5wrnynuzfxzsz9xhrz9xzvz3" ) } } + +/* +The Python script below can be used to generate the byte arrays. + +Just replace the argument to toBase32BitString() with the string you want to encode. If the length +of the resulting bit string is not a multiple of 8 then you will need to pad the string, like this: + bitstring = toBase32BitString("aa") + "000000" # Add zeroes to pad the string to a valid length + +import io + +ALPHABET = "23456789abcdefghjkmnopqrstuvwxyz" + +def toBase32BitString(s): + buf = io.StringIO() + for c in s: + alphabetIndex = ALPHABET.index(c) + buf.write(f"{alphabetIndex:05b}") + return buf.getvalue() + +bitstring = toBase32BitString("badmoods") + +values = [] +for i in range(0, len(bitstring), 8): + chunk = bitstring[i:i+8] + if len(chunk) != 8: + raise ValueError(f"invalid chunk size at {i}: {len(chunk)} (expected exactly 8)") + intvalue = int(chunk, 2) + values.append(intvalue if intvalue <= 127 else (intvalue - 256)) + +print(values) +*/ From 5c43869818cb94ee67cdb80f7c28ef60729e46fb Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 1 Dec 2023 14:28:28 -0500 Subject: [PATCH 102/573] QuerySubscription.kt: make it work with QueryManager (#519) --- .../QuerySubscriptionIntegrationTest.kt | 72 ++-- .../generated/PostsIntegrationTest.kt | 4 +- .../dataconnect/DataConnectGrpcClient.kt | 86 ++-- .../firebase/dataconnect/DataConnectResult.kt | 10 +- .../dataconnect/FirebaseDataConnect.kt | 28 +- .../firebase/dataconnect/QueryManager.kt | 385 ++++++++++-------- .../google/firebase/dataconnect/QueryRef.kt | 3 +- .../firebase/dataconnect/QuerySubscription.kt | 93 ++--- .../com/google/firebase/dataconnect/Util.kt | 14 + .../dataconnect/generated/GetPostQuery.kt | 2 +- .../dataconnect/DataConnectResultTest.kt | 250 ++++++++++-- 11 files changed, 613 insertions(+), 334 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt index 9a6c2e2e633..f6acc69d033 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt @@ -30,8 +30,6 @@ import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQu import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.subscribe import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute import java.util.concurrent.CopyOnWriteArrayList -import java.util.concurrent.Executors -import kotlin.math.max import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.* import kotlinx.coroutines.channels.* @@ -60,7 +58,7 @@ class QuerySubscriptionIntegrationTest { fun lastResult_should_be_equal_to_the_last_collected_result() = runTest { schema.createPerson.execute(id = "TestId", name = "TestPerson", age = 42) val querySubscription = schema.getPerson.subscribe(id = "42") - val result = querySubscription.flow.first() + val result = querySubscription.resultFlow.first() assertThat(querySubscription.lastResult).isEqualTo(result) } @@ -73,7 +71,7 @@ class QuerySubscriptionIntegrationTest { Channel>( capacity = Channel.UNLIMITED ) - backgroundScope.launch { querySubscription.flow.collect(resultsChannel::send) } + backgroundScope.launch { querySubscription.resultFlow.collect(resultsChannel::send) } val result1 = resultsChannel.receive() assertThat(result1.data.person).isEqualToGetPersonQueryResult(name = "Name0", age = 10000) @@ -87,16 +85,16 @@ class QuerySubscriptionIntegrationTest { @Test fun flow_collect_should_get_immediately_invoked_with_last_result() = runTest { - schema.createPerson.execute(id = "TestId12345", name = "TestName", age = 10000) + schema.createPerson.execute(id = "TestId12345", name = "OriginalName", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - val result1 = querySubscription.flow.first().data.person - assertThat(result1).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) + val result1 = querySubscription.resultFlow.first().data.person + assertWithMessage("result1.name").that(result1!!.name).isEqualTo("OriginalName") - schema.updatePerson.execute(id = "TestId12345", name = "TestName2", age = 10002) + schema.updatePerson.execute(id = "TestId12345", name = "UpdatedName") - val result2 = querySubscription.flow.first().data.person - assertThat(result2).isEqualToGetPersonQueryResult(name = "TestName", age = 10000) + val result2 = querySubscription.resultFlow.first().data.person + assertWithMessage("result2.name").that(result2!!.name).isEqualTo("OriginalName") } @Test @@ -104,11 +102,13 @@ class QuerySubscriptionIntegrationTest { schema.createPerson.execute(id = "TestId12345", name = "TestName", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - backgroundScope.launch { querySubscription.flow.collect { delay(Integer.MAX_VALUE.seconds) } } + backgroundScope.launch { + querySubscription.resultFlow.collect { delay(Integer.MAX_VALUE.seconds) } + } repeat(5) { assertWithMessage("fast flow retrieval iteration $it") - .that(querySubscription.flow.first().data.person) + .that(querySubscription.resultFlow.first().data.person) .isEqualToGetPersonQueryResult(name = "TestName", age = 10000) } } @@ -120,9 +120,9 @@ class QuerySubscriptionIntegrationTest { val querySubscription = schema.getPerson.subscribe(queryVariables) val results1 = CopyOnWriteArrayList>() val results2 = CopyOnWriteArrayList>() - backgroundScope.launch { querySubscription.flow.toList(results1) } + backgroundScope.launch { querySubscription.resultFlow.toList(results1) } delayUntil("results1.isNotEmpty()") { results1.isNotEmpty() } - backgroundScope.launch { querySubscription.flow.toList(results2) } + backgroundScope.launch { querySubscription.resultFlow.toList(results2) } delayUntil("results2.isNotEmpty()") { results2.isNotEmpty() } schema.updatePerson.execute(id = "TestId12345", name = "TestName9", age = 99999) @@ -134,13 +134,15 @@ class QuerySubscriptionIntegrationTest { DataConnectResult( variables = queryVariables, data = GetPersonQueryData(GetPersonQueryData.Person(name = "TestName0", age = 10000)), - errors = emptyList() + errors = emptyList(), + sequenceNumber = -1, // sequenceNumber is not considered by equals() ) val expectedResult2 = DataConnectResult( variables = queryVariables, data = GetPersonQueryData(GetPersonQueryData.Person(name = "TestName9", age = 99999)), - errors = emptyList() + errors = emptyList(), + sequenceNumber = -1, // sequenceNumber is not considered by equals() ) assertWithMessage("results1") .that(results1) @@ -157,24 +159,34 @@ class QuerySubscriptionIntegrationTest { schema.createPerson.execute(id = "TestId12345", name = "Name", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - val resultsChannel = Channel(capacity = Channel.UNLIMITED) - backgroundScope.launch { querySubscription.flow.map { it.data }.collect(resultsChannel::send) } + val collectedResults = + CopyOnWriteArrayList>() + backgroundScope.launch { querySubscription.resultFlow.toList(collectedResults) } + + val deferreds = buildList { + repeat(25_000) { + // Use `Dispatchers.Default` as the dispatcher for the launched coroutines so that there + // will be at least 2 threads used to run the coroutines (as documented by + // `Dispatchers.Default`), introducing a guaranteed minimum level of parallelism, ensuring + // that this test is indeed testing "massive concurrency". + add(backgroundScope.async(Dispatchers.Default) { querySubscription.reload() }) + } + } - val maxHardwareConcurrency = max(2, Runtime.getRuntime().availableProcessors()) - val multiThreadExecutor = Executors.newFixedThreadPool(maxHardwareConcurrency) - try { - repeat(100000) { multiThreadExecutor.execute(querySubscription::reload) } - } finally { - multiThreadExecutor.shutdown() + // Wait for at least one result to come in. + while (collectedResults.isEmpty()) { + yield() } - var resultCount = 0 - while (true) { - resultCount++ - val result = withTimeoutOrNull(1.seconds) { resultsChannel.receive() } ?: break - assertThat(result.person).isEqualToGetPersonQueryResult(name = "Name", age = 10000) + // Wait for all calls to reload() to complete. + deferreds.forEach { it.await() } + + // Verify that we got the expected results. + collectedResults.forEachIndexed { index, result -> + assertWithMessage("collectedResults[$index]") + .that(result.data.person) + .isEqualToGetPersonQueryResult(name = "Name", age = 10000) } - assertThat(resultCount).isGreaterThan(0) } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt index 865c480e23b..ab52c360fa8 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt @@ -78,7 +78,7 @@ class PostsIntegrationTest { val querySubscription = posts.getPost.subscribe(id = postId1) assertWithMessage("lastResult 0").that(querySubscription.lastResult).isNull() - val result1 = querySubscription.flow.first() + val result1 = querySubscription.resultFlow.first() assertWithMessage("result1.isSuccess").that(result1.errors).isEmpty() assertWithMessage("result1.post.content") .that(result1.data.post?.content) @@ -86,7 +86,7 @@ class PostsIntegrationTest { assertWithMessage("lastResult 1").that(querySubscription.lastResult).isEqualTo(result1) - val flow2Job = backgroundScope.async { querySubscription.flow.take(2).toList() } + val flow2Job = backgroundScope.async { querySubscription.resultFlow.take(2).toList() } querySubscription.update { id = postId2 } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 0759b29ed0a..ac226c00d68 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -84,28 +84,28 @@ internal class DataConnectGrpcClient( logger.warn(e) { "Failed to update ssl context" } } - ManagedChannelBuilder.forAddress(hostName, port).let { - if (!sslEnabled) { - it.usePlaintext() - } + val channel = + ManagedChannelBuilder.forAddress(hostName, port).let { + if (!sslEnabled) { + it.usePlaintext() + } - // Ensure gRPC recovers from a dead connection. This is not typically necessary, as - // the OS will usually notify gRPC when a connection dies. But not always. This acts as a - // failsafe. - it.keepAliveTime(30, TimeUnit.SECONDS) + // Ensure gRPC recovers from a dead connection. This is not typically necessary, as + // the OS will usually notify gRPC when a connection dies. But not always. This acts as a + // failsafe. + it.keepAliveTime(30, TimeUnit.SECONDS) - it.executor(executor) + it.executor(executor) - // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel - // to - // respond more gracefully to network change events, such as switching from cellular to - // wifi. - val channel = AndroidChannelBuilder.usingBuilder(it).context(context).build() + // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel + // to respond more gracefully to network change events, such as switching from cellular to + // wifi. + AndroidChannelBuilder.usingBuilder(it).context(context).build() + } - logger.debug { "${ManagedChannel::class.qualifiedName} initialization completed" } + logger.debug { "${ManagedChannel::class.qualifiedName} initialization completed" } - channel - } + channel } private val grpcChannelOrNull @@ -117,13 +117,22 @@ internal class DataConnectGrpcClient( private val grpcStub: DataServiceCoroutineStub by lazy(LazyThreadSafetyMode.NONE) { DataServiceCoroutineStub(grpcChannel.value) } - data class OperationResult(val data: Struct?, val errors: List) - data class DeserialzedOperationResult(val data: T, val errors: List) + data class OperationResult( + val data: Struct?, + val errors: List, + val sequenceNumber: Long + ) + data class DeserialzedOperationResult( + val data: T, + val errors: List, + val sequenceNumber: Long + ) suspend fun executeQuery( requestId: String, + sequenceNumber: Long, operationName: String, - variables: Struct + variables: Struct, ): OperationResult { val request = executeQueryRequest { this.name = requestName @@ -152,14 +161,16 @@ internal class DataConnectGrpcClient( return OperationResult( data = if (response.hasData()) response.data else null, - errors = response.errorsList.map { it.toDataConnectError() } + errors = response.errorsList.map { it.toDataConnectError() }, + sequenceNumber = sequenceNumber, ) } suspend fun executeMutation( requestId: String, + sequenceNumber: Long, operationName: String, - variables: Struct + variables: Struct, ): OperationResult { val request = executeMutationRequest { this.name = requestName @@ -188,7 +199,8 @@ internal class DataConnectGrpcClient( return OperationResult( data = if (response.hasData()) response.data else null, - errors = response.errorsList.map { it.toDataConnectError() } + errors = response.errorsList.map { it.toDataConnectError() }, + sequenceNumber = sequenceNumber, ) } @@ -238,17 +250,23 @@ internal fun GraphqlError.toDataConnectError() = internal fun DataConnectGrpcClient.OperationResult.deserialize( dataDeserializer: DeserializationStrategy -): DataConnectGrpcClient.DeserialzedOperationResult { - if (data === null) { - // TODO: include the variables and error list in the thrown exception - throw DataConnectException("no data included in result: errors=${errors}") - } - return DataConnectGrpcClient.DeserialzedOperationResult( - data = decodeFromStruct(dataDeserializer, data), - errors = errors - ) -} +): DataConnectGrpcClient.DeserialzedOperationResult = + if (data === null) + // TODO: include the variables and error list in the thrown exception + throw DataConnectException("no data included in result: errors=${errors}") + else + DataConnectGrpcClient.DeserialzedOperationResult( + data = decodeFromStruct(dataDeserializer, data), + errors = errors, + sequenceNumber = sequenceNumber, + ) internal fun DataConnectGrpcClient.DeserialzedOperationResult.toDataConnectResult( variables: V -) = DataConnectResult(variables = variables, data = data, errors = errors) +) = + DataConnectResult( + variables = variables, + data = data, + errors = errors, + sequenceNumber = sequenceNumber + ) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt index 9cef48dd8b6..7c68b29f9d8 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt @@ -14,13 +14,17 @@ package com.google.firebase.dataconnect class DataConnectResult -private constructor(private val impl: Impl) { +private constructor( + private val impl: Impl, + internal val sequenceNumber: Long +) { internal constructor( variables: VariablesType, data: DataType, - errors: List - ) : this(Impl(variables = variables, data = data, errors = errors)) + errors: List, + sequenceNumber: Long, + ) : this(Impl(variables = variables, data = data, errors = errors), sequenceNumber) val variables: VariablesType get() = impl.variables diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index b026b4cfc16..1a8854cbc5d 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -20,19 +20,10 @@ import com.google.firebase.FirebaseApp import com.google.firebase.app import java.util.concurrent.Executor import kotlin.random.Random -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancel +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.serialization.DeserializationStrategy @@ -69,6 +60,8 @@ internal constructor( } ) + private val blockingDispatcher = blockingExecutor.asCoroutineDispatcher() + // Protects `closed`, `grpcClient`, and `queryManager`. private val mutex = Mutex() @@ -103,17 +96,15 @@ internal constructor( get() = if (grpcClient.isInitialized()) grpcClient.value else null // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference - // // to the lazily-created object is obtained, then the mutex can be unlocked and the instance - // can - // // be used. - private val queryManager = + // to the lazily-created object is obtained, then the mutex can be unlocked and the instance can + // be used. + private val queryManager by lazy(LazyThreadSafetyMode.NONE) { if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") - QueryManager(grpcClient.value, coroutineScope, logger.id) + QueryManager(grpcClient.value, coroutineScope, blockingDispatcher, logger.id) } - internal suspend fun executeQuery(ref: QueryRef, variables: V) = - mutex.withLock { queryManager.value }.execute(ref, variables) + internal suspend fun getQueryManager(): QueryManager = mutex.withLock { queryManager } internal suspend fun executeMutation(ref: MutationRef, variables: V) = executeMutation(ref, variables, requestId = Random.nextAlphanumericString()) @@ -127,10 +118,11 @@ internal constructor( .withLock { grpcClient.value } .executeMutation( requestId = requestId, + sequenceNumber = nextSequenceNumber(), operationName = ref.operationName, variables = encodeToStruct(ref.variablesSerializer, variables) ) - .runCatching { deserialize(ref.dataDeserializer) } + .runCatching { withContext(blockingDispatcher) { deserialize(ref.dataDeserializer) } } .onFailure { logger.warn(it) { "executeMutation() [rid=$requestId] decoding response data failed: $it" } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 4602e7b06d2..c383bf82a68 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -18,53 +18,44 @@ import com.google.firebase.dataconnect.DataConnectGrpcClient.OperationResult import com.google.protobuf.Struct import java.util.concurrent.CopyOnWriteArrayList import kotlin.random.Random -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.async -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.* import kotlinx.serialization.DeserializationStrategy internal class QueryManager( grpcClient: DataConnectGrpcClient, coroutineScope: CoroutineScope, + deserializationDispatcher: CoroutineDispatcher, creatorLoggerId: String ) { private val logger = Logger("QueryManager").apply { debug { "Created from $creatorLoggerId" } } - private val queryStates = QueryStates(grpcClient, coroutineScope, logger) + private val queryStates = + QueryStates(grpcClient, coroutineScope, deserializationDispatcher, logger) suspend fun execute(ref: QueryRef, variables: V): DataConnectResult = queryStates .withQueryState(ref, variables) { it.execute(ref.dataDeserializer) } .toDataConnectResult(variables) - suspend fun collectResults( + suspend fun onResult( ref: QueryRef, variables: V, - collector: FlowCollector> - ) { - queryStates.withQueryState(ref, variables) { - it.collectResults(ref.dataDeserializer, collector) { toDataConnectResult(variables) } - } - } - - suspend fun collectExceptions( - ref: QueryRef, - variables: V, - collector: FlowCollector - ) { - queryStates.withQueryState(ref, variables) { - it.collectExceptions(ref.dataDeserializer, collector) + sinceSequenceNumber: Long?, + executeQuery: Boolean, + callback: suspend (DataConnectResult) -> Unit, + ): Nothing = + queryStates.withQueryState(ref, variables) { queryState -> + queryState.onResult( + ref.dataDeserializer, + sinceSequenceNumber = sinceSequenceNumber, + executeQuery = executeQuery + ) { + callback(it.toDataConnectResult(variables)) + } } - } } private data class QueryStateKey(val operationName: String, val variablesHash: String) @@ -75,42 +66,38 @@ private class QueryState( private val operationName: String, private val variables: Struct, private val coroutineScope: CoroutineScope, + private val deserializationDispatcher: CoroutineDispatcher, private val logger: Logger, ) { - private val mutex = Mutex() - private var job: Deferred? = null + // The `dataDeserializers` list may be safely read concurrently from multiple threads, as it uses + // a `CopyOnWriteArrayList` that is completely thread-safe. Any mutating operations must be + // performed while the `dataDeserializersWriteMutex` mutex is locked, so that + // read-write-modify operations can be done atomically. + private val dataDeserializersWriteMutex = Mutex() + private val dataDeserializers = CopyOnWriteArrayList>() - private val dataDeserializers = CopyOnWriteArrayList>() - - private val operationResultFlow = - MutableSharedFlow( - replay = 1, - extraBufferCapacity = Int.MAX_VALUE, - onBufferOverflow = BufferOverflow.SUSPEND - ) - - private val exceptionFlow = - MutableSharedFlow( - replay = 1, - extraBufferCapacity = Int.MAX_VALUE, - onBufferOverflow = BufferOverflow.SUSPEND - ) + private val jobMutex = Mutex() + private var job: Job? = null suspend fun execute( dataDeserializer: DeserializationStrategy ): DeserialzedOperationResult { + // Register the data deserialzier _before_ waiting for the current job to complete. This + // guarantees that the deserializer will be registered by the time the subsequent job (`newJob` + // below) runs. + val registeredDataDeserialzer = + registerDataDeserializer(dataDeserializer, deserializationDispatcher) + // Wait for the current job to complete (if any), and ignore its result. Waiting avoids running // multiple queries in parallel, which would not scale. - val originalJob = mutex.withLock { job }?.also { it.join() } + val originalJob = jobMutex.withLock { job }?.also { it.join() } // Now that the job that was in progress when this method started has completed, we can run our // own query. But we're racing with other concurrent invocations of this method. The first one - // wins and launches the new job, then awaits its completion; the others simply await - // completion of the new job that was started by the winner. + // wins and launches the new job, then awaits its completion; the others simply await completion + // of the new job that was started by the winner. val newJob = - mutex.withLock { - registerDataDeserializer(dataDeserializer) - + jobMutex.withLock { job.let { currentJob -> if (currentJob !== null && currentJob !== originalJob) { currentJob @@ -120,124 +107,198 @@ private class QueryState( } } - // TODO: As an optimization, avoid calling deserialize() if the data was already deserialized - // by someone else. - return newJob.await().deserialize(dataDeserializer) + newJob.join() + + return registeredDataDeserialzer.getLatestUpdate()!!.getOrThrow() } - private suspend fun doExecute(): OperationResult { + suspend fun onResult( + dataDeserializer: DeserializationStrategy, + sinceSequenceNumber: Long?, + executeQuery: Boolean, + callback: suspend (DeserialzedOperationResult) -> Unit, + ): Nothing { + val registeredDataDeserialzer = + registerDataDeserializer(dataDeserializer, deserializationDispatcher) + + // Immediately deliver the most recent update to the callback, so the collector has some data + // to work with while waiting for the network requests to complete. + val cachedUpdate = registeredDataDeserialzer.getLatestSuccessfulUpdate() + val effectiveSinceSequenceNumber = + if (cachedUpdate === null) { + sinceSequenceNumber + } else if ( + sinceSequenceNumber !== null && sinceSequenceNumber >= cachedUpdate.sequenceNumber + ) { + sinceSequenceNumber + } else { + callback(cachedUpdate) + cachedUpdate.sequenceNumber + } + + // Execute the query _after_ delivering the cached result, so that collectors deterministically + // get invoked with cached results first (if any), then updated results after the query + // executes. + if (executeQuery) { + coroutineScope.launch { runCatching { execute(dataDeserializer) } } + } + + registeredDataDeserialzer.onSuccessfulUpdate( + sinceSequenceNumber = effectiveSinceSequenceNumber + ) { + callback(it) + } + } + + private suspend fun doExecute() { val requestId = Random.nextAlphanumericString() + val sequenceNumber = nextSequenceNumber() val executeQueryResult = grpcClient.runCatching { - executeQuery(requestId = requestId, operationName = operationName, variables = variables) + executeQuery( + requestId = requestId, + operationName = operationName, + variables = variables, + sequenceNumber = sequenceNumber + ) } - mutex - .withLock { dataDeserializers.iterator() } - .forEach { it.update(requestId, executeQueryResult) } - - return executeQueryResult.fold( - onSuccess = { - operationResultFlow.emit(it) - exceptionFlow.emit(null) - it - }, - onFailure = { - exceptionFlow.emit(it) - throw it - } - ) + dataDeserializers.iterator().forEach { + it.update(requestId, sequenceNumber, executeQueryResult) + } } - suspend fun collectResults( - dataDeserializer: DeserializationStrategy, - collector: FlowCollector, - mapResult: DeserialzedOperationResult.() -> R - ) = - mutex - .withLock { registerDataDeserializer(dataDeserializer) } - .resultFlow - .map { mapResult(it) } - .collect(collector) - - suspend fun collectExceptions( - dataDeserializer: DeserializationStrategy<*>, - collector: FlowCollector - ) = - mutex - .withLock { registerDataDeserializer(dataDeserializer) } - .exceptionFlow - .map { it } - .collect(collector) - - // NOTE: This function MUST be called by a coroutine that has `mutex` locked; otherwise, a data - // race will occur, resulting in undefined behavior. @Suppress("UNCHECKED_CAST") - private fun registerDataDeserializer( - dataDeserializer: DeserializationStrategy - ): DeserialzerInfo = - dataDeserializers.firstOrNull { it.deserializer === dataDeserializer } as? DeserialzerInfo - ?: DeserialzerInfo(dataDeserializer, logger).also { dataDeserializers.add(it) } - - private class DeserialzerInfo( - val deserializer: DeserializationStrategy, - private val logger: Logger - ) { - private val _resultFlow = - MutableSharedFlow>( - replay = 1, - extraBufferCapacity = Int.MAX_VALUE, - onBufferOverflow = BufferOverflow.SUSPEND, - ) - - val resultFlow = _resultFlow.asSharedFlow() - - val _exceptionFlow = - MutableSharedFlow( - replay = 1, - extraBufferCapacity = Int.MAX_VALUE, - onBufferOverflow = BufferOverflow.SUSPEND, - ) - - val exceptionFlow = _exceptionFlow.asSharedFlow() - - suspend fun update(requestId: String, result: Result) { - result.fold( - onFailure = { _exceptionFlow.emit(it) }, - onSuccess = { operationResult -> - operationResult - .runCatching { deserialize(deserializer) } - .fold( - onSuccess = { deserializedOperationResult -> - _resultFlow.emit(deserializedOperationResult) - _exceptionFlow.emit(null) - }, - onFailure = { deserializeException -> - logger.warn(deserializeException) { - "executeQuery() [rid=$requestId] " + - "decoding response data failed: $deserializeException" - } - _exceptionFlow.emit(deserializeException) - } - ) - }, - ) + private suspend fun registerDataDeserializer( + dataDeserializer: DeserializationStrategy, + deserializationDispatcher: CoroutineDispatcher, + ): RegisteredDataDeserialzer = + // First, check if the deserializer is already registered and, if it is, just return it. + // Otherwise, lock the "write" mutex and register it. We still have to check again if it is + // already registered because another thread could have concurrently registered it since we last + // checked above. + dataDeserializers + .firstOrNull { it.deserializer === dataDeserializer } + ?.let { it as RegisteredDataDeserialzer } + ?: dataDeserializersWriteMutex.withLock { + dataDeserializers + .firstOrNull { it.deserializer === dataDeserializer } + ?.let { it as RegisteredDataDeserialzer } + ?: RegisteredDataDeserialzer(dataDeserializer, deserializationDispatcher, logger).also { + dataDeserializers.add(it) + } + } +} + +private class RegisteredDataDeserialzer( + val deserializer: DeserializationStrategy, + private val deserializationDispatcher: CoroutineDispatcher, + private val logger: Logger +) { + private val latestUpdate = MutableStateFlow?>(null) + private val latestSuccessfulUpdate = MutableStateFlow?>(null) + + data class Update( + val sequenceNumber: Long, + val result: Lazy>> + ) + + fun update(requestId: String, sequenceNumber: Long, result: Result) { + // Use a compare-and-swap ("CAS") loop to ensure that an old update never clobbers a newer one. + val newUpdate = + Update(sequenceNumber = sequenceNumber, result = lazyDeserialize(requestId, result)) + while (true) { + val currentUpdate = latestUpdate.value + if (currentUpdate !== null && currentUpdate.sequenceNumber > sequenceNumber) { + return // don't clobber a newer update with an older one + } + if (latestUpdate.compareAndSet(currentUpdate, newUpdate)) { + return + } } } + + suspend fun getLatestUpdate(): Result>? { + val lazyResult = latestUpdate.value?.result ?: return null + return if (lazyResult.isInitialized()) { + lazyResult.value + } else withContext(deserializationDispatcher) { lazyResult.value } + } + + suspend fun getLatestSuccessfulUpdate(): DeserialzedOperationResult? { + // Call getLatestUpdate() to populate `latestSuccessfulUpdate` with the most recent update. + getLatestUpdate() + return latestSuccessfulUpdate.value + } + + suspend fun onSuccessfulUpdate( + sinceSequenceNumber: Long?, + callback: suspend (DeserialzedOperationResult) -> Unit + ): Nothing { + var lastSequenceNumber = sinceSequenceNumber ?: Long.MIN_VALUE + latestUpdate.collect { + if (it !== null && lastSequenceNumber < it.sequenceNumber) { + val update = + if (it.result.isInitialized()) { + it.result.value + } else withContext(deserializationDispatcher) { it.result.value } + update.onSuccess { + lastSequenceNumber = it.sequenceNumber + callback(it) + } + } + } + } + + private fun lazyDeserialize( + requestId: String, + result: Result + ): Lazy>> = lazy { + result + .mapCatching { + // TODO: move the deserialization to a different dispatcher because it could take a decent + // amount of time. As a general rule of thumb, suspend functions shouldn't perform blocking + // I/O or long-running CPU-bound operations on the calling thread since the calling thread + // could be the main thread. + it.deserialize(deserializer) + } + .onFailure { + // If the overall result was successful then the failure _must_ have occurred during + // deserialization. Log the deserialization failure so it doesn't go unnoticed. + if (result.isSuccess) { + logger.warn(it) { "executeQuery() [rid=$requestId] decoding response data failed: $it" } + } + } + .onSuccess { + // Update the latest successful update. Set the value in a compare-and-swap loop to ensure + // that an older result does not clobber a newer one. + while (true) { + val latestSuccessful = latestSuccessfulUpdate.value + if (latestSuccessful !== null && latestSuccessful.sequenceNumber >= it.sequenceNumber) { + break + } + if (latestSuccessfulUpdate.compareAndSet(latestSuccessful, it)) { + break + } + } + } + } } private class QueryStates( private val grpcClient: DataConnectGrpcClient, private val coroutineScope: CoroutineScope, + private val deserializationDispatcher: CoroutineDispatcher, private val logger: Logger, ) { private val mutex = Mutex() - // NOTE: All accesses to `queryStateByKey` and the `refCount` field of each value MUST be done - // from a coroutine that has locked `mutex`; otherwise, such accesses (both reads and writes) are - // data races and yield undefined behavior. - private val queryStateByKey = mutableMapOf>() + // NOTE: All accesses to `referenceCountedQueryStateByKey` and the `refCount` field of each value + // MUST be done from a coroutine that has locked `mutex`; otherwise, such accesses (both reads and + // writes) are data races and yield undefined behavior. + private val referenceCountedQueryStateByKey = + mutableMapOf>() suspend fun withQueryState( ref: QueryRef, @@ -259,8 +320,8 @@ private class QueryStates( val variablesHash = variablesStruct.calculateSha512().toAlphaNumericString() val key = QueryStateKey(operationName = ref.operationName, variablesHash = variablesHash) - val queryState = - queryStateByKey.getOrPut(key) { + val referenceCountedQueryState = + referenceCountedQueryStateByKey.getOrPut(key) { ReferenceCounted( QueryState( key = key, @@ -268,33 +329,31 @@ private class QueryStates( operationName = ref.operationName, variables = variablesStruct, coroutineScope = coroutineScope, + deserializationDispatcher = deserializationDispatcher, logger = logger, ), refCount = 0 ) } - queryState.refCount++ + referenceCountedQueryState.refCount++ - return queryState.obj + return referenceCountedQueryState.obj } // NOTE: This function MUST be called from a coroutine that has locked `mutex`. private fun releaseQueryState(queryState: QueryState) { - val referenceCountedQueryState = - queryStateByKey[queryState.key].let { - if (it === null) { - error("unexpected null QueryState for key: ${queryState.key}") - } else if (it.obj !== queryState) { - error("unexpected QueryState for key: ${queryState.key}: $it") - } else { - it - } - } + val referenceCountedQueryState = referenceCountedQueryStateByKey[queryState.key] + + if (referenceCountedQueryState === null) { + error("unexpected null QueryState for key: ${queryState.key}") + } else if (referenceCountedQueryState.obj !== queryState) { + error("unexpected QueryState for key: ${queryState.key}: ${referenceCountedQueryState.obj}") + } referenceCountedQueryState.refCount-- if (referenceCountedQueryState.refCount == 0) { - queryStateByKey.remove(queryState.key) + referenceCountedQueryStateByKey.remove(queryState.key) } } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt index e65d24253f7..ec597c04a00 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt @@ -31,7 +31,8 @@ internal constructor( ) { override suspend fun execute( variables: VariablesType - ): DataConnectResult = dataConnect.executeQuery(this, variables) + ): DataConnectResult = + dataConnect.getQueryManager().execute(this, variables) fun subscribe(variables: VariablesType): QuerySubscription = QuerySubscription(this, variables) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt index 7e18beb9f00..0d8c4747393 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt @@ -13,10 +13,7 @@ // limitations under the License. package com.google.firebase.dataconnect -import com.google.firebase.concurrent.FirebaseExecutors -import java.util.concurrent.atomic.AtomicReference import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* class QuerySubscription @@ -24,60 +21,64 @@ internal constructor( internal val query: QueryRef, variables: VariablesType ) { - private val _variables = AtomicReference(variables) - val variables: VariablesType - get() = _variables.get() + private val _variables = MutableStateFlow(variables) + val variables: VariablesType by _variables.asStateFlow()::value - private val sharedFlow = - MutableSharedFlow>( - replay = 1, - extraBufferCapacity = Integer.MAX_VALUE - ) - private val sequentialDispatcher = - FirebaseExecutors.newSequentialExecutor(query.dataConnect.nonBlockingExecutor) - .asCoroutineDispatcher() + private val _lastResult = MutableStateFlow?>(null) + val lastResult: DataConnectResult? by _lastResult.asStateFlow()::value - // NOTE: The variables below must ONLY be accessed from coroutines that use `sequentialDispatcher` - // for their `CoroutineDispatcher`. Having this requirement removes the need for explicitly - // synchronizing access to these variables. - private var inProgressReload: CompletableDeferred>? = - null - private var pendingReload: CompletableDeferred>? = null + // Each collection of this flow triggers an implicit `reload()`. + val resultFlow: Flow> = channelFlow { + val cachedResult = lastResult?.also { send(it) } - val lastResult: DataConnectResult? - get() = sharedFlow.replayCache.firstOrNull() - - fun reload(): Deferred> = - runBlocking(sequentialDispatcher) { - pendingReload - ?: run { - CompletableDeferred>().also { deferred -> - if (inProgressReload == null) { - inProgressReload = deferred - query.dataConnect.coroutineScope.launch(sequentialDispatcher) { doReloadLoop() } - } else { - pendingReload = deferred - } + var collectJob: Job? = null + _variables.collect { variables -> + // We only need to execute the query upon initially collecting the flow. Subsequent changes to + // the variables automatically get a call to reload() by update(). + val shouldExecuteQuery = + collectJob.let { + if (it === null) { + true + } else { + it.cancelAndJoin() + false } } + + collectJob = launch { + query.dataConnect.getQueryManager().onResult( + query, + variables, + sinceSequenceNumber = cachedResult?.sequenceNumber, + executeQuery = shouldExecuteQuery + ) { + updateLastResult(it) + send(it) + } + } } + } - fun update(variables: VariablesType) { - _variables.set(variables) - reload() + suspend fun reload() { + query.dataConnect.getQueryManager().execute(query, variables) } - val flow: Flow> - get() = sharedFlow.asSharedFlow().onSubscription { reload() }.buffer(Channel.CONFLATED) + suspend fun update(variables: VariablesType) { + _variables.value = variables + reload() + } - private suspend fun doReloadLoop() { + private fun updateLastResult(newLastResult: DataConnectResult) { + // Update the last result in a compare-and-swap loop so that there is no possibility of + // clobbering a newer result with an older result, compared using their sequence numbers. while (true) { - val deferred = inProgressReload ?: break - val result = query.execute(variables) - deferred.complete(result) - sharedFlow.emit(result) - inProgressReload = pendingReload - pendingReload = null + val oldLastResult = _lastResult.value + if (oldLastResult !== null && oldLastResult.sequenceNumber > newLastResult.sequenceNumber) { + return + } + if (_lastResult.compareAndSet(oldLastResult, newLastResult)) { + return + } } } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 282525fb5b3..3adcc12e17a 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -14,6 +14,7 @@ package com.google.firebase.dataconnect import java.io.OutputStream +import java.util.concurrent.atomic.AtomicLong import kotlin.math.abs import kotlin.random.Random @@ -25,6 +26,19 @@ internal object NullOutputStream : OutputStream() { internal class ReferenceCounted(val obj: T, var refCount: Int) +private val nextSequenceId = AtomicLong(0) + +/** + * Returns a positive number on each invocation, with each returned value being strictly greater + * than any value previously returned in this process. + * + * This function is thread-safe and may be called concurrently by multiple threads and/or + * coroutines. + */ +internal fun nextSequenceNumber(): Long { + return nextSequenceId.incrementAndGet() +} + /** * Generates and returns a string containing random alphanumeric characters. * diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt index fc7bbd4762b..c3bff071aef 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/GetPostQuery.kt @@ -54,7 +54,7 @@ suspend fun QueryRef.execute(id: Stri fun QueryRef.subscribe(id: String) = subscribe(variablesFor(id = id)) -fun QuerySubscription.update( +suspend fun QuerySubscription.update( block: GetPostQuery.Variables.Builder.() -> Unit ) = update(variables.build(block)) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt index 7fb600d006c..03c4bab0230 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt @@ -11,7 +11,12 @@ class DataConnectResultTest { fun `variables should be the same object given to the constructor`() { val variables = TestVariables("boo") val dataConnectResult = - DataConnectResult(variables = variables, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = variables, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.variables).isSameInstanceAs(variables) } @@ -19,7 +24,12 @@ class DataConnectResultTest { fun `data should be the same object given to the constructor`() { val data = TestData("blah") val dataConnectResult = - DataConnectResult(variables = SAMPLE_VARIABLES, data = data, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = data, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.data).isSameInstanceAs(data) } @@ -27,25 +37,48 @@ class DataConnectResultTest { fun `errors should be the same object given to the constructor`() { val errors = listOf(SAMPLE_ERROR1) val dataConnectResult = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = errors) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = errors, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.errors).isSameInstanceAs(errors) } @Test fun `errors should be empty if empty was given to the constructor`() { val dataConnectResult = - DataConnectResult( + DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = emptyList() + errors = emptyList(), + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult.errors).isEmpty() } + @Test + fun `sequenceNumber should be the same object given to the constructor`() { + val dataConnectResult = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = 12345 + ) + assertThat(dataConnectResult.sequenceNumber).isEqualTo(12345) + } + @Test fun `toString() should begin with the class name and contain text in parentheses`() { val dataConnectResult = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.toString()).startsWith("DataConnectResult(") assertThat(dataConnectResult.toString()).endsWith(")") } @@ -57,7 +90,12 @@ class DataConnectResultTest { override fun toString() = "TestVariablesToString" } val dataConnectResult = - DataConnectResult(variables = variables, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = variables, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.toString()).containsWithNonAdjacentText("TestVariablesToString") } @@ -68,7 +106,12 @@ class DataConnectResultTest { override fun toString() = "TestDataToString" } val dataConnectResult = - DataConnectResult(variables = SAMPLE_VARIABLES, data = data, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = data, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.toString()).containsWithNonAdjacentText("TestDataToString") } @@ -76,54 +119,130 @@ class DataConnectResultTest { fun `toString() should incorporate the errors`() { val errors = listOf(SAMPLE_ERROR1, SAMPLE_ERROR2) val dataConnectResult = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = errors) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = errors, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.toString()).containsWithNonAdjacentText(errors.toString()) } @Test fun `toString() should incorporate the errors, if empty`() { val dataConnectResult = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = emptyList()) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = emptyList(), + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.toString()) .containsWithNonAdjacentText(emptyList().toString()) } + @Test + fun `toString() should NOT incorporate the sequenceNumber`() { + val dataConnectResult = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = 123456789123456789 + ) + assertThat(dataConnectResult.toString()).doesNotContain("123456789123456789") + } + @Test fun `equals() should return true for the exact same instance`() { val dataConnectResult = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.equals(dataConnectResult)).isTrue() } @Test fun `equals() should return true for an equal instance`() { val dataConnectResult1 = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) val dataConnectResult2 = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isTrue() } @Test fun `equals() should return true if all properties are equal, and data is null`() { val dataConnectResult1 = - DataConnectResult(variables = SAMPLE_VARIABLES, data = null, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = null, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) val dataConnectResult2 = - DataConnectResult(variables = SAMPLE_VARIABLES, data = null, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = null, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) + assertThat(dataConnectResult1.equals(dataConnectResult2)).isTrue() + } + + @Test + fun `equals() should return true for equal instances with different sequenceNumber`() { + val dataConnectResult1 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = 1 + ) + val dataConnectResult2 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = 2 + ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isTrue() } @Test fun `equals() should return false for null`() { val dataConnectResult = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.equals(null)).isFalse() } @Test fun `equals() should return false for a different type`() { val dataConnectResult = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult.equals(listOf("foo"))).isFalse() } @@ -133,13 +252,15 @@ class DataConnectResultTest { DataConnectResult( variables = TestVariables("foo"), data = SAMPLE_DATA, - errors = SAMPLE_ERRORS + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = TestVariables("bar"), data = SAMPLE_DATA, - errors = SAMPLE_ERRORS + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() } @@ -150,13 +271,15 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("foo"), - errors = SAMPLE_ERRORS + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("bar"), - errors = SAMPLE_ERRORS + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() } @@ -164,12 +287,18 @@ class DataConnectResultTest { @Test fun `equals() should return false when data of first object is null`() { val dataConnectResult1 = - DataConnectResult(variables = SAMPLE_VARIABLES, data = null, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = null, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("bar"), - errors = SAMPLE_ERRORS + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() } @@ -180,10 +309,16 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("foo"), - errors = SAMPLE_ERRORS + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = - DataConnectResult(variables = SAMPLE_VARIABLES, data = null, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = null, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() } @@ -193,13 +328,15 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = listOf(SAMPLE_ERROR1) + errors = listOf(SAMPLE_ERROR1), + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = listOf(SAMPLE_ERROR2) + errors = listOf(SAMPLE_ERROR2), + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() } @@ -207,7 +344,12 @@ class DataConnectResultTest { @Test fun `hashCode() should return the same value each time it is invoked on a given object`() { val dataConnectResult = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) val hashCode = dataConnectResult.hashCode() assertThat(dataConnectResult.hashCode()).isEqualTo(hashCode) assertThat(dataConnectResult.hashCode()).isEqualTo(hashCode) @@ -217,9 +359,38 @@ class DataConnectResultTest { @Test fun `hashCode() should return the same value on equal objects`() { val dataConnectResult1 = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) val dataConnectResult2 = - DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, errors = SAMPLE_ERRORS) + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER + ) + assertThat(dataConnectResult1.hashCode()).isEqualTo(dataConnectResult2.hashCode()) + } + + @Test + fun `hashCode() should return the same value on equal objects, even if sequenceNumber differs`() { + val dataConnectResult1 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = 1 + ) + val dataConnectResult2 = + DataConnectResult( + variables = SAMPLE_VARIABLES, + data = SAMPLE_DATA, + errors = SAMPLE_ERRORS, + sequenceNumber = 2 + ) assertThat(dataConnectResult1.hashCode()).isEqualTo(dataConnectResult2.hashCode()) } @@ -229,13 +400,15 @@ class DataConnectResultTest { DataConnectResult( variables = TestVariables("foo"), data = SAMPLE_DATA, - errors = SAMPLE_ERRORS + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = TestVariables("bar"), data = SAMPLE_DATA, - errors = SAMPLE_ERRORS + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.hashCode()).isNotEqualTo(dataConnectResult2.hashCode()) } @@ -246,13 +419,15 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("foo"), - errors = SAMPLE_ERRORS + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("bar"), - errors = SAMPLE_ERRORS + errors = SAMPLE_ERRORS, + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.hashCode()).isNotEqualTo(dataConnectResult2.hashCode()) } @@ -263,13 +438,15 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = listOf(SAMPLE_ERROR1) + errors = listOf(SAMPLE_ERROR1), + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = listOf(SAMPLE_ERROR2) + errors = listOf(SAMPLE_ERROR2), + sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.hashCode()).isNotEqualTo(dataConnectResult2.hashCode()) } @@ -302,3 +479,4 @@ private val SAMPLE_ERROR2 = extensions = SAMPLE_ERROR_EXTENSIONS2 ) private val SAMPLE_ERRORS = listOf(SAMPLE_ERROR1, SAMPLE_ERROR2) +private val SAMPLE_SEQUENCE_NUMBER: Long = -1 From d65753ed910cfe8cd199b307cc643b1df4dd07f6 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 4 Dec 2023 11:05:07 -0500 Subject: [PATCH 103/573] EmulatorController.kt: include the actual path to the operation set whose parsing failed. --- .../testutil/EmulatorController.kt | 65 ++++++++++++------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt index 033eb4240da..138c14399f0 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt @@ -15,13 +15,14 @@ import java.lang.Exception import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withContext +data class EmulatorSchemaInfo(val filePath: String, val contents: String) + suspend fun FirebaseDataConnect.installEmulatorSchema( - schema: String, - operationSets: Map + schema: EmulatorSchemaInfo, + operationSets: Map ) { val (hostName, port) = settings.run { Pair(hostName, port) } @@ -46,24 +47,32 @@ suspend fun FirebaseDataConnect.installEmulatorSchema( suspend fun FirebaseDataConnect.installEmulatorSchema(assetDir: String) { val assets = app.applicationContext.assets - val schemaFileName = "schema.gql" - val schema = + val schemaFileName = "schema.gql" + val schemaPath = "$assetDir/$schemaFileName" + val schemaContents = withContext(Dispatchers.IO) { - assets.open("$assetDir/$schemaFileName").use { inputStream -> + assets.open(schemaPath).use { inputStream -> InputStreamReader(inputStream, Charsets.UTF_8).readText() } } + val schema = EmulatorSchemaInfo(filePath = schemaPath, contents = schemaContents) val loadedAssets = - loadAssets(assets, assetDir) { it.endsWith(".gql") && it != schemaFileName } - .flowOn(Dispatchers.IO) - .toList() + loadAssets(assets, assetDir) { it.endsWith(".gql") && it != schemaFileName }.toList() - val operationSets = mutableMapOf() - loadedAssets.forEach { - val operationSet = it.fileName.run { substring(0, length - 4) } - operationSets[operationSet] = it.contents + val operationSets = buildMap { + loadedAssets.forEach { + val operationSetName = + it.filePath.run { + val startIndex = + it.filePath.lastIndexOf('/').let { lastSlashIndex -> + if (lastSlashIndex < 0) 0 else (lastSlashIndex + 1) + } + substring(startIndex, length - 4) + } + put(operationSetName, EmulatorSchemaInfo(filePath = it.filePath, contents = it.contents)) + } } installEmulatorSchema(schema = schema, operationSets = operationSets) @@ -71,25 +80,31 @@ suspend fun FirebaseDataConnect.installEmulatorSchema(assetDir: String) { private fun loadAssets(assets: AssetManager, dirPath: String, filter: (String) -> Boolean) = flow { val fileNames = - assets.list(dirPath) ?: throw NoSuchAssetError("AssetManager.list($dirPath) returned null") + withContext(Dispatchers.IO) { + assets.list(dirPath) ?: throw NoSuchAssetError("AssetManager.list($dirPath) returned null") + } + fileNames.filter(filter).forEach { fileName -> + val assetPath = "$dirPath/$fileName" val contents = - assets.open("$dirPath/$fileName").use { inputStream -> - InputStreamReader(inputStream, Charsets.UTF_8).readText() + withContext(Dispatchers.IO) { + assets.open(assetPath).use { inputStream -> + InputStreamReader(inputStream, Charsets.UTF_8).readText() + } } - emit(LoadedAsset(fileName = fileName, contents = contents)) + emit(LoadedAsset(filePath = assetPath, contents = contents)) } } -private data class LoadedAsset(val fileName: String, val contents: String) +private data class LoadedAsset(val filePath: String, val contents: String) private class NoSuchAssetError(message: String) : Exception(message) private suspend fun setupSchema( grpcStub: EmulatorServiceCoroutineStub, serviceId: String, - schema: String, - operationSets: Map + schema: EmulatorSchemaInfo, + operationSets: Map ) { grpcStub.setupSchema( setupSchemaRequest { @@ -97,19 +112,19 @@ private suspend fun setupSchema( this.schema = source { this.files.add( file { - this.path = "schema/schema.gql" - this.content = schema + this.path = schema.filePath + this.content = schema.contents } ) } - operationSets.forEach { (operationSetName, queriesAndMutations) -> + operationSets.forEach { (operationSetName, emulatorSchemaInfo) -> this.operationSets.put( operationSetName, source { this.files.add( file { - this.path = "schema/queriesAndMutations.gql" - this.content = queriesAndMutations + this.path = emulatorSchemaInfo.filePath + this.content = emulatorSchemaInfo.contents } ) } From fb2c51ccf2561a93e3fb2e1f78782f6e43f35f60 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 4 Dec 2023 15:13:22 -0500 Subject: [PATCH 104/573] QueryRefIntegrationTest.kt: add tests for nested structs --- .../testing_graphql_schemas/alltypes/ops.gql | 34 ++++++ .../alltypes/schema.gql | 21 ++++ .../dataconnect/QueryRefIntegrationTest.kt | 101 ++++++++++++++++ .../testutil/schemas/AllTypesSchema.kt | 110 ++++++++++++++++++ 4 files changed, 266 insertions(+) diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql index a01e7b196ed..c5d9bfd4b46 100644 --- a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql @@ -64,3 +64,37 @@ query getAllPrimitiveLists @auth(is: PUBLIC) { stringListNullable } } + +mutation createFarmer($data: Farmer_Data! @pick(fields: ["id", "name", "parentId"])) @auth(is: PUBLIC) { + farmer_insert(data: $data) +} + +mutation createAnimal($data: Animal_Data! @pick(fields: ["id", "farmId", "name", "species", "age"])) @auth(is: PUBLIC) { + animal_insert(data: $data) +} + +mutation createFarm($data: Farm_Data! @pick(fields: ["id", "name", "farmerId"])) @auth(is: PUBLIC) { + farm_insert(data: $data) +} + +query getFarm($id: ID!) @auth(is: PUBLIC) { + farm(id: $id) { + id + name + farmer { + id + name + parent { + id + name + parentId + } + } + animals: animals_as_farm { + id + name + species + age + } + } +} \ No newline at end of file diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql index e800bb736a2..305fa7763c9 100644 --- a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql @@ -37,3 +37,24 @@ type PrimitiveList @table { stringList: [String!] stringListNullable: [String] } + +type Farm @table { + id: ID! + name: String! + farmer: Farmer! +} + +type Animal @table { + id: ID! + farm: Farm! + name: String! + species: String! + age: Int +} + +type Farmer @table { + id: ID! + name: String! + parent: Farmer +} + diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt index 0d67769022a..cf7838edd04 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -202,6 +202,107 @@ class QueryRefIntegrationTest { assertThat(result.errors).isEmpty() } + @Test + fun executeWithNestedTypesInData() = runTest { + allTypesSchema.createFarmer(id = "Farmer1Id", name = "Farmer1Name", parentId = null) + allTypesSchema.createFarmer(id = "Farmer2Id", name = "Farmer2Name", parentId = "Farmer1Id") + allTypesSchema.createFarmer(id = "Farmer3Id", name = "Farmer3Name", parentId = "Farmer2Id") + allTypesSchema.createFarmer(id = "Farmer4Id", name = "Farmer4Name", parentId = "Farmer3Id") + allTypesSchema.createFarm(id = "FarmId", name = "TestFarm", farmerId = "Farmer4Id") + allTypesSchema.createAnimal( + id = "Animal1Id", + farmId = "FarmId", + name = "Animal1Name", + species = "Animal1Species", + age = 1 + ) + allTypesSchema.createAnimal( + id = "Animal2Id", + farmId = "FarmId", + name = "Animal2Name", + species = "Animal2Species", + age = 2 + ) + allTypesSchema.createAnimal( + id = "Animal3Id", + farmId = "FarmId", + name = "Animal3Name", + species = "Animal3Species", + age = 3 + ) + allTypesSchema.createAnimal( + id = "Animal4Id", + farmId = "FarmId", + name = "Animal4Name", + species = "Animal4Species", + age = null + ) + + val result = allTypesSchema.getFarm("FarmId") + + assertWithMessage("result.data.farm").that(result.data.farm).isNotNull() + val farm = result.data.farm!! + assertThat(farm.id).isEqualTo("FarmId") + assertThat(farm.name).isEqualTo("TestFarm") + assertWithMessage("farm.farmer") + .that(farm.farmer) + .isEqualTo( + AllTypesSchema.GetFarmQuery.Farmer( + id = "Farmer4Id", + name = "Farmer4Name", + parent = + AllTypesSchema.GetFarmQuery.Parent( + id = "Farmer3Id", + name = "Farmer3Name", + parentId = "Farmer2Id", + ) + ) + ) + assertWithMessage("farm.animals") + .that(farm.animals) + .containsExactly( + AllTypesSchema.GetFarmQuery.Animal( + id = "Animal1Id", + name = "Animal1Name", + species = "Animal1Species", + age = 1 + ), + AllTypesSchema.GetFarmQuery.Animal( + id = "Animal2Id", + name = "Animal2Name", + species = "Animal2Species", + age = 2 + ), + AllTypesSchema.GetFarmQuery.Animal( + id = "Animal3Id", + name = "Animal3Name", + species = "Animal3Species", + age = 3 + ), + AllTypesSchema.GetFarmQuery.Animal( + id = "Animal4Id", + name = "Animal4Name", + species = "Animal4Species", + age = null + ), + ) + } + + @Test + fun executeWithNestedNullTypesInData() = runTest { + allTypesSchema.createFarmer(id = "Farmer1Id", name = "Farmer1Name", parentId = null) + allTypesSchema.createFarm(id = "FarmId", name = "TestFarm", farmerId = "Farmer1Id") + + val result = allTypesSchema.getFarm("FarmId") + + assertWithMessage("result.data.farm").that(result.data.farm).isNotNull() + result.data.farm!!.apply { + assertThat(id).isEqualTo("FarmId") + assertThat(name).isEqualTo("TestFarm") + assertWithMessage("farm.farmer.parent").that(farmer.parent).isNull() + } + } + @Test fun executeShouldThrowIfDataConnectInstanceIsClosed() = runTest { personSchema.dataConnect.close() diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt index 2d8e664c61e..5a8a767e2eb 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt @@ -130,6 +130,116 @@ class AllTypesSchema(val dataConnect: FirebaseDataConnect) { dataDeserializer = serializer() ) + object CreateFarmerMutation { + @Serializable data class Variables(val data: Farmer) + + @Serializable + data class Farmer( + val id: String, + val name: String, + val parentId: String?, + ) + } + + val createFarmer = + dataConnect.mutation( + operationName = "createFarmer", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + + suspend fun createFarmer(id: String, name: String, parentId: String?) = + createFarmer.execute( + CreateFarmerMutation.Variables( + CreateFarmerMutation.Farmer(id = id, name = name, parentId = parentId) + ) + ) + + object CreateFarmMutation { + @Serializable data class Variables(val data: Farm) + + @Serializable data class Farm(val id: String, val name: String, val farmerId: String?) + } + + val createFarm = + dataConnect.mutation( + operationName = "createFarm", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + + suspend fun createFarm(id: String, name: String, farmerId: String?) = + createFarm.execute( + CreateFarmMutation.Variables( + CreateFarmMutation.Farm(id = id, name = name, farmerId = farmerId) + ) + ) + + object CreateAnimalMutation { + @Serializable data class Variables(val data: Animal) + + @Serializable + data class Animal( + val id: String, + val farmId: String, + val name: String, + val species: String, + val age: Int? + ) + } + + val createAnimal = + dataConnect.mutation( + operationName = "createAnimal", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + + suspend fun createAnimal(id: String, farmId: String, name: String, species: String, age: Int?) = + createAnimal.execute( + CreateAnimalMutation.Variables( + CreateAnimalMutation.Animal( + id = id, + farmId = farmId, + name = name, + species = species, + age = age + ) + ) + ) + + object GetFarmQuery { + @Serializable data class Variables(val id: String) + + @Serializable data class Data(val farm: Farm?) + + @Serializable + data class Farm( + val id: String, + val name: String, + val farmer: Farmer, + val animals: List + ) + + @Serializable data class Farmer(val id: String, val name: String, val parent: Parent?) + + @Serializable data class Parent(val id: String, val name: String, val parentId: String?) + + @Serializable + data class Animal(val id: String, val name: String, val species: String, val age: Int?) + + suspend fun QueryRef.execute(id: String) = execute(Variables(id = id)) + } + + val getFarm = + dataConnect.query( + operationName = "getFarm", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + + suspend fun getFarm(id: String) = getFarm.execute(GetFarmQuery.Variables(id = id)) + companion object { const val OPERATION_SET = "ops" From 832597d675ba42edf3ff4f8f492648c5605c692a Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 4 Dec 2023 15:17:28 -0500 Subject: [PATCH 105/573] AllTypesSchema.kt: use Double instead of Float, since Double is the native wire format --- .../dataconnect/QueryRefIntegrationTest.kt | 18 +++++++++--------- .../testutil/schemas/AllTypesSchema.kt | 12 ++++++++---- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt index cf7838edd04..945ef47e763 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -108,8 +108,8 @@ class QueryRefIntegrationTest { idFieldNullable = "TestNullableId", intField = 42, intFieldNullable = 43, - floatField = 123.45f, - floatFieldNullable = 678.91f, + floatField = 123.45, + floatFieldNullable = 678.91, booleanField = true, booleanFieldNullable = false, stringField = "TestString", @@ -125,8 +125,8 @@ class QueryRefIntegrationTest { assertThat(primitive.idFieldNullable).isEqualTo("TestNullableId") assertThat(primitive.intField).isEqualTo(42) assertThat(primitive.intFieldNullable).isEqualTo(43) - assertThat(primitive.floatField).isEqualTo(123.45f) - assertThat(primitive.floatFieldNullable).isEqualTo(678.91f) + assertThat(primitive.floatField).isEqualTo(123.45) + assertThat(primitive.floatFieldNullable).isEqualTo(678.91) assertThat(primitive.booleanField).isEqualTo(true) assertThat(primitive.booleanFieldNullable).isEqualTo(false) assertThat(primitive.stringField).isEqualTo("TestString") @@ -143,7 +143,7 @@ class QueryRefIntegrationTest { idFieldNullable = null, intField = 42, intFieldNullable = null, - floatField = 123.45f, + floatField = 123.45, floatFieldNullable = null, booleanField = true, booleanFieldNullable = null, @@ -174,8 +174,8 @@ class QueryRefIntegrationTest { idListNullable = listOf("ddd", "eee"), intList = listOf(42, 43, 44), intListNullable = listOf(45, 46), - floatList = listOf(12.3f, 45.6f, 78.9f), - floatListNullable = listOf(98.7f, 65.4f), + floatList = listOf(12.3, 45.6, 78.9), + floatListNullable = listOf(98.7, 65.4), booleanList = listOf(true, false, true, false), booleanListNullable = listOf(false, true, false, true), stringList = listOf("xxx", "yyy", "zzz"), @@ -193,8 +193,8 @@ class QueryRefIntegrationTest { assertThat(primitive.idListNullable).containsExactly("ddd", "eee").inOrder() assertThat(primitive.intList).containsExactly(42, 43, 44).inOrder() assertThat(primitive.intListNullable).containsExactly(45, 46).inOrder() - assertThat(primitive.floatList).containsExactly(12.3f, 45.6f, 78.9f).inOrder() - assertThat(primitive.floatListNullable).containsExactly(98.7f, 65.4f).inOrder() + assertThat(primitive.floatList).containsExactly(12.3, 45.6, 78.9).inOrder() + assertThat(primitive.floatListNullable).containsExactly(98.7, 65.4).inOrder() assertThat(primitive.booleanList).containsExactly(true, false, true, false).inOrder() assertThat(primitive.booleanListNullable).containsExactly(false, true, false, true).inOrder() assertThat(primitive.stringList).containsExactly("xxx", "yyy", "zzz").inOrder() diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt index 5a8a767e2eb..7065d304b5e 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt @@ -43,8 +43,10 @@ class AllTypesSchema(val dataConnect: FirebaseDataConnect) { val idFieldNullable: String?, val intField: Int, val intFieldNullable: Int?, - val floatField: Float, - val floatFieldNullable: Float?, + // NOTE: GraphQL "Float" type is a "signed double-precision floating-point value", which is + // equivalent to Java and Kotlin's `Double` type. + val floatField: Double, + val floatFieldNullable: Double?, val booleanField: Boolean, val booleanFieldNullable: Boolean?, val stringField: String, @@ -83,8 +85,10 @@ class AllTypesSchema(val dataConnect: FirebaseDataConnect) { val idListNullable: List, val intList: List, val intListNullable: List, - val floatList: List, - val floatListNullable: List, + // NOTE: GraphQL "Float" type is a "signed double-precision floating-point value", which is + // equivalent to Java and Kotlin's `Double` type. + val floatList: List, + val floatListNullable: List, val booleanList: List, val booleanListNullable: List, val stringList: List, From 86b27690ca3bd3499b6ae257e0b367674b66ec3e Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 4 Dec 2023 15:37:39 -0500 Subject: [PATCH 106/573] AllTypesSchema.kt: add nullable lists and lists of nullables --- .../testing_graphql_schemas/alltypes/ops.gql | 12 +++++++++- .../alltypes/schema.gql | 23 +++++++++++-------- .../dataconnect/QueryRefIntegrationTest.kt | 14 +++++++++-- .../testutil/schemas/AllTypesSchema.kt | 15 ++++++++---- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql index c5d9bfd4b46..a2bd24c4703 100644 --- a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/ops.gql @@ -31,7 +31,7 @@ query getPrimitive($id: ID!) @auth(is: PUBLIC) { } } -mutation createPrimitiveList($data: PrimitiveList_Data! @pick(fields: ["id", "idListNullable", "intList", "intListNullable", "floatList", "floatListNullable", "booleanList", "booleanListNullable", "stringList", "stringListNullable"])) @auth(is: PUBLIC) { +mutation createPrimitiveList($data: PrimitiveList_Data! @pick(fields: ["id", "idListNullable", "idListOfNullable", "intList", "intListNullable", "intListOfNullable", "floatList", "floatListNullable", "floatListOfNullable", "booleanList", "booleanListNullable", "booleanListOfNullable", "stringList", "stringListNullable", "stringListOfNullable"])) @auth(is: PUBLIC) { primitiveList_insert(data: $data) } @@ -39,14 +39,19 @@ query getPrimitiveList($id: ID!) @auth(is: PUBLIC) { primitiveList(id: $id) { id idListNullable + idListOfNullable intList intListNullable + intListOfNullable floatList floatListNullable + floatListOfNullable booleanList booleanListNullable + booleanListOfNullable stringList stringListNullable + stringListOfNullable } } @@ -54,14 +59,19 @@ query getAllPrimitiveLists @auth(is: PUBLIC) { primitiveLists { id idListNullable + idListOfNullable intList intListNullable + intListOfNullable floatList floatListNullable + floatListOfNullable booleanList booleanListNullable + booleanListOfNullable stringList stringListNullable + stringListOfNullable } } diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql index 305fa7763c9..fae6f62447c 100644 --- a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/alltypes/schema.gql @@ -27,15 +27,20 @@ type Primitive @table { type PrimitiveList @table { id: ID! - idListNullable: [ID] - intList: [Int!] - intListNullable: [Int] - floatList: [Float!] - floatListNullable: [Float] - booleanList: [Boolean!] - booleanListNullable: [Boolean] - stringList: [String!] - stringListNullable: [String] + idListNullable: [ID!] + idListOfNullable: [ID]! + intList: [Int!]! + intListNullable: [Int!] + intListOfNullable: [Int]! + floatList: [Float!]! + floatListNullable: [Float!] + floatListOfNullable: [Float]! + booleanList: [Boolean!]! + booleanListNullable: [Boolean!] + booleanListOfNullable: [Boolean]! + stringList: [String!]! + stringListNullable: [String!] + stringListOfNullable: [String]! } type Farm @table { diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt index 945ef47e763..11c798c8d44 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -171,15 +171,20 @@ class QueryRefIntegrationTest { AllTypesSchema.CreatePrimitiveListMutation.Variables( AllTypesSchema.PrimitiveListData( id = "TestId", - idListNullable = listOf("ddd", "eee"), + idListNullable = listOf("aaa", "bbb"), + idListOfNullable = listOf("ccc", "ddd"), intList = listOf(42, 43, 44), intListNullable = listOf(45, 46), + intListOfNullable = listOf(47, 48), floatList = listOf(12.3, 45.6, 78.9), floatListNullable = listOf(98.7, 65.4), + floatListOfNullable = listOf(100.1, 100.2), booleanList = listOf(true, false, true, false), booleanListNullable = listOf(false, true, false, true), + booleanListOfNullable = listOf(false, false, true, true), stringList = listOf("xxx", "yyy", "zzz"), stringListNullable = listOf("qqq", "rrr"), + stringListOfNullable = listOf("sss", "ttt"), ) ) ) @@ -190,15 +195,20 @@ class QueryRefIntegrationTest { val primitive = result.data.primitiveList ?: error("result.data.primitiveList is null") assertThat(primitive.id).isEqualTo("TestId") - assertThat(primitive.idListNullable).containsExactly("ddd", "eee").inOrder() + assertThat(primitive.idListNullable).containsExactly("aaa", "bbb").inOrder() + assertThat(primitive.idListOfNullable).containsExactly("ccc", "ddd").inOrder() assertThat(primitive.intList).containsExactly(42, 43, 44).inOrder() assertThat(primitive.intListNullable).containsExactly(45, 46).inOrder() + assertThat(primitive.intListOfNullable).containsExactly(47, 48).inOrder() assertThat(primitive.floatList).containsExactly(12.3, 45.6, 78.9).inOrder() assertThat(primitive.floatListNullable).containsExactly(98.7, 65.4).inOrder() + assertThat(primitive.floatListOfNullable).containsExactly(100.1, 100.2).inOrder() assertThat(primitive.booleanList).containsExactly(true, false, true, false).inOrder() assertThat(primitive.booleanListNullable).containsExactly(false, true, false, true).inOrder() + assertThat(primitive.booleanListOfNullable).containsExactly(false, false, true, true).inOrder() assertThat(primitive.stringList).containsExactly("xxx", "yyy", "zzz").inOrder() assertThat(primitive.stringListNullable).containsExactly("qqq", "rrr").inOrder() + assertThat(primitive.stringListOfNullable).containsExactly("sss", "ttt").inOrder() assertThat(result.errors).isEmpty() } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt index 7065d304b5e..29e63120585 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt @@ -82,17 +82,22 @@ class AllTypesSchema(val dataConnect: FirebaseDataConnect) { @Serializable data class PrimitiveListData( val id: String, - val idListNullable: List, + val idListNullable: List?, + val idListOfNullable: List, val intList: List, - val intListNullable: List, + val intListNullable: List?, + val intListOfNullable: List, // NOTE: GraphQL "Float" type is a "signed double-precision floating-point value", which is // equivalent to Java and Kotlin's `Double` type. val floatList: List, - val floatListNullable: List, + val floatListNullable: List?, + val floatListOfNullable: List, val booleanList: List, - val booleanListNullable: List, + val booleanListNullable: List?, + val booleanListOfNullable: List, val stringList: List, - val stringListNullable: List, + val stringListNullable: List?, + val stringListOfNullable: List, ) object CreatePrimitiveListMutation { From 0e69b5e50d3c6bb211e5ef35dab46af435bb0940 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 4 Dec 2023 15:41:31 -0500 Subject: [PATCH 107/573] DataConnectGrpcClient.kt: throw exceptions if errors list is non empty, rather than returning them --- .../dataconnect/QueryRefIntegrationTest.kt | 8 - .../QuerySubscriptionIntegrationTest.kt | 2 - .../generated/PostsIntegrationTest.kt | 3 - .../dataconnect/DataConnectGrpcClient.kt | 41 ++-- .../firebase/dataconnect/DataConnectResult.kt | 8 +- .../dataconnect/DataConnectResultTest.kt | 195 ++---------------- 6 files changed, 40 insertions(+), 217 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt index 11c798c8d44..119acd70f0b 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -57,7 +57,6 @@ class QueryRefIntegrationTest { assertThat(result.data.person?.name).isEqualTo("TestName2") assertThat(result.data.person?.age).isEqualTo(43) - assertThat(result.errors).isEmpty() } @Test @@ -69,7 +68,6 @@ class QueryRefIntegrationTest { assertThat(result.data.person?.name).isEqualTo("NewTestName") assertThat(result.data.person?.age).isEqualTo(99) - assertThat(result.errors).isEmpty() } @Test @@ -79,7 +77,6 @@ class QueryRefIntegrationTest { val result = personSchema.getPerson.execute(id = "NotTheTestId") assertThat(result.data.person).isNull() - assertThat(result.errors).isEmpty() } @Test @@ -96,7 +93,6 @@ class QueryRefIntegrationTest { Person(id = "TestId2", name = "TestName2", age = 43), Person(id = "TestId3", name = "TestName3", age = 44), ) - assertThat(result.errors).isEmpty() } @Test @@ -131,7 +127,6 @@ class QueryRefIntegrationTest { assertThat(primitive.booleanFieldNullable).isEqualTo(false) assertThat(primitive.stringField).isEqualTo("TestString") assertThat(primitive.stringFieldNullable).isEqualTo("TestNullableString") - assertThat(result.errors).isEmpty() } @Test @@ -161,7 +156,6 @@ class QueryRefIntegrationTest { assertThat(primitive.floatFieldNullable).isNull() assertThat(primitive.booleanFieldNullable).isNull() assertThat(primitive.stringFieldNullable).isNull() - assertThat(result.errors).isEmpty() } @Test @@ -209,7 +203,6 @@ class QueryRefIntegrationTest { assertThat(primitive.stringList).containsExactly("xxx", "yyy", "zzz").inOrder() assertThat(primitive.stringListNullable).containsExactly("qqq", "rrr").inOrder() assertThat(primitive.stringListOfNullable).containsExactly("sss", "ttt").inOrder() - assertThat(result.errors).isEmpty() } @Test @@ -341,7 +334,6 @@ class QueryRefIntegrationTest { val results = deferreds.map { it.await() } results.forEachIndexed { index, result -> assertWithMessage("results[$index]").that(result.data.person).isNull() - assertWithMessage("results[$index]").that(result.errors).isEmpty() } } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt index f6acc69d033..f2c7e44c204 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt @@ -134,14 +134,12 @@ class QuerySubscriptionIntegrationTest { DataConnectResult( variables = queryVariables, data = GetPersonQueryData(GetPersonQueryData.Person(name = "TestName0", age = 10000)), - errors = emptyList(), sequenceNumber = -1, // sequenceNumber is not considered by equals() ) val expectedResult2 = DataConnectResult( variables = queryVariables, data = GetPersonQueryData(GetPersonQueryData.Person(name = "TestName9", age = 99999)), - errors = emptyList(), sequenceNumber = -1, // sequenceNumber is not considered by equals() ) assertWithMessage("results1") diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt index ab52c360fa8..c5d0626b91a 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/generated/PostsIntegrationTest.kt @@ -79,7 +79,6 @@ class PostsIntegrationTest { assertWithMessage("lastResult 0").that(querySubscription.lastResult).isNull() val result1 = querySubscription.resultFlow.first() - assertWithMessage("result1.isSuccess").that(result1.errors).isEmpty() assertWithMessage("result1.post.content") .that(result1.data.post?.content) .isEqualTo(postContent1) @@ -92,8 +91,6 @@ class PostsIntegrationTest { val results2 = flow2Job.await() assertWithMessage("results2.size").that(results2.size).isEqualTo(2) - assertWithMessage("results2[0].isSuccess").that(results2[0].errors).isEmpty() - assertWithMessage("results2[1].isSuccess").that(results2[1].errors).isEmpty() assertWithMessage("results2[0].post.content") .that(results2[0].data.post?.content) .isEqualTo(postContent1) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index ac226c00d68..0ee94c1ae70 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -122,11 +122,8 @@ internal class DataConnectGrpcClient( val errors: List, val sequenceNumber: Long ) - data class DeserialzedOperationResult( - val data: T, - val errors: List, - val sequenceNumber: Long - ) + + data class DeserialzedOperationResult(val data: T, val sequenceNumber: Long) suspend fun executeQuery( requestId: String, @@ -250,23 +247,23 @@ internal fun GraphqlError.toDataConnectError() = internal fun DataConnectGrpcClient.OperationResult.deserialize( dataDeserializer: DeserializationStrategy -): DataConnectGrpcClient.DeserialzedOperationResult = - if (data === null) - // TODO: include the variables and error list in the thrown exception - throw DataConnectException("no data included in result: errors=${errors}") - else - DataConnectGrpcClient.DeserialzedOperationResult( - data = decodeFromStruct(dataDeserializer, data), - errors = errors, - sequenceNumber = sequenceNumber, - ) +): DataConnectGrpcClient.DeserialzedOperationResult { + val deserializedData: T = + if (data === null) { + // TODO: include the variables and error list in the thrown exception + throw DataConnectException("no data included in result: errors=$errors") + } else if (errors.isNotEmpty()) { + throw DataConnectException("operation failed: errors=$errors") + } else { + decodeFromStruct(dataDeserializer, data) + } + + return DataConnectGrpcClient.DeserialzedOperationResult( + data = deserializedData, + sequenceNumber = sequenceNumber, + ) +} internal fun DataConnectGrpcClient.DeserialzedOperationResult.toDataConnectResult( variables: V -) = - DataConnectResult( - variables = variables, - data = data, - errors = errors, - sequenceNumber = sequenceNumber - ) +) = DataConnectResult(variables = variables, data = data, sequenceNumber = sequenceNumber) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt index 7c68b29f9d8..705552d3a28 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectResult.kt @@ -22,26 +22,22 @@ private constructor( internal constructor( variables: VariablesType, data: DataType, - errors: List, sequenceNumber: Long, - ) : this(Impl(variables = variables, data = data, errors = errors), sequenceNumber) + ) : this(Impl(variables = variables, data = data), sequenceNumber) val variables: VariablesType get() = impl.variables val data: DataType get() = impl.data - val errors: List - get() = impl.errors override fun hashCode() = impl.hashCode() override fun equals(other: Any?) = (other as? DataConnectResult<*, *>)?.let { it.impl == impl } ?: false - override fun toString() = "DataConnectResult(variables=$variables, data=$data, errors=$errors)" + override fun toString() = "DataConnectResult(variables=$variables, data=$data)" private data class Impl( val variables: VariablesType, val data: DataType, - val errors: List, ) } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt index 03c4bab0230..148aa6d70d2 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectResultTest.kt @@ -1,7 +1,6 @@ package com.google.firebase.dataconnect import com.google.common.truth.Truth.assertThat -import com.google.firebase.dataconnect.DataConnectError.PathSegment import com.google.firebase.dataconnect.testutil.containsWithNonAdjacentText import org.junit.Test @@ -14,7 +13,6 @@ class DataConnectResultTest { DataConnectResult( variables = variables, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult.variables).isSameInstanceAs(variables) @@ -27,46 +25,16 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = data, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult.data).isSameInstanceAs(data) } - @Test - fun `errors should be the same object given to the constructor`() { - val errors = listOf(SAMPLE_ERROR1) - val dataConnectResult = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = errors, - sequenceNumber = SAMPLE_SEQUENCE_NUMBER - ) - assertThat(dataConnectResult.errors).isSameInstanceAs(errors) - } - - @Test - fun `errors should be empty if empty was given to the constructor`() { - val dataConnectResult = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = emptyList(), - sequenceNumber = SAMPLE_SEQUENCE_NUMBER - ) - assertThat(dataConnectResult.errors).isEmpty() - } - @Test fun `sequenceNumber should be the same object given to the constructor`() { val dataConnectResult = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, - sequenceNumber = 12345 - ) + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, sequenceNumber = 12345) + assertThat(dataConnectResult.sequenceNumber).isEqualTo(12345) } @@ -76,7 +44,6 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult.toString()).startsWith("DataConnectResult(") @@ -93,7 +60,6 @@ class DataConnectResultTest { DataConnectResult( variables = variables, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult.toString()).containsWithNonAdjacentText("TestVariablesToString") @@ -109,45 +75,17 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = data, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult.toString()).containsWithNonAdjacentText("TestDataToString") } - @Test - fun `toString() should incorporate the errors`() { - val errors = listOf(SAMPLE_ERROR1, SAMPLE_ERROR2) - val dataConnectResult = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = errors, - sequenceNumber = SAMPLE_SEQUENCE_NUMBER - ) - assertThat(dataConnectResult.toString()).containsWithNonAdjacentText(errors.toString()) - } - - @Test - fun `toString() should incorporate the errors, if empty`() { - val dataConnectResult = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = emptyList(), - sequenceNumber = SAMPLE_SEQUENCE_NUMBER - ) - assertThat(dataConnectResult.toString()) - .containsWithNonAdjacentText(emptyList().toString()) - } - @Test fun `toString() should NOT incorporate the sequenceNumber`() { val dataConnectResult = DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = 123456789123456789 ) assertThat(dataConnectResult.toString()).doesNotContain("123456789123456789") @@ -159,7 +97,6 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult.equals(dataConnectResult)).isTrue() @@ -171,14 +108,12 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isTrue() @@ -190,14 +125,12 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = null, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = null, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isTrue() @@ -206,19 +139,11 @@ class DataConnectResultTest { @Test fun `equals() should return true for equal instances with different sequenceNumber`() { val dataConnectResult1 = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, - sequenceNumber = 1 - ) + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, sequenceNumber = 1) + val dataConnectResult2 = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, - sequenceNumber = 2 - ) + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, sequenceNumber = 2) + assertThat(dataConnectResult1.equals(dataConnectResult2)).isTrue() } @@ -228,7 +153,6 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult.equals(null)).isFalse() @@ -240,7 +164,6 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult.equals(listOf("foo"))).isFalse() @@ -252,14 +175,12 @@ class DataConnectResultTest { DataConnectResult( variables = TestVariables("foo"), data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = TestVariables("bar"), data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() @@ -271,14 +192,12 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("foo"), - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("bar"), - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() @@ -290,14 +209,12 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = null, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("bar"), - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() @@ -309,33 +226,12 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("foo"), - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = null, - errors = SAMPLE_ERRORS, - sequenceNumber = SAMPLE_SEQUENCE_NUMBER - ) - assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() - } - - @Test - fun `equals() should return false when only errors differs`() { - val dataConnectResult1 = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = listOf(SAMPLE_ERROR1), - sequenceNumber = SAMPLE_SEQUENCE_NUMBER - ) - val dataConnectResult2 = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = listOf(SAMPLE_ERROR2), sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.equals(dataConnectResult2)).isFalse() @@ -347,7 +243,6 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val hashCode = dataConnectResult.hashCode() @@ -362,14 +257,12 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.hashCode()).isEqualTo(dataConnectResult2.hashCode()) @@ -378,19 +271,11 @@ class DataConnectResultTest { @Test fun `hashCode() should return the same value on equal objects, even if sequenceNumber differs`() { val dataConnectResult1 = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, - sequenceNumber = 1 - ) + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, sequenceNumber = 1) + val dataConnectResult2 = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, - sequenceNumber = 2 - ) + DataConnectResult(variables = SAMPLE_VARIABLES, data = SAMPLE_DATA, sequenceNumber = 2) + assertThat(dataConnectResult1.hashCode()).isEqualTo(dataConnectResult2.hashCode()) } @@ -400,14 +285,12 @@ class DataConnectResultTest { DataConnectResult( variables = TestVariables("foo"), data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = TestVariables("bar"), data = SAMPLE_DATA, - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.hashCode()).isNotEqualTo(dataConnectResult2.hashCode()) @@ -419,64 +302,24 @@ class DataConnectResultTest { DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("foo"), - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) val dataConnectResult2 = DataConnectResult( variables = SAMPLE_VARIABLES, data = TestData("bar"), - errors = SAMPLE_ERRORS, sequenceNumber = SAMPLE_SEQUENCE_NUMBER ) assertThat(dataConnectResult1.hashCode()).isNotEqualTo(dataConnectResult2.hashCode()) } - @Test - fun `hashCode() should return a different value if errors is different`() { - val dataConnectResult1 = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = listOf(SAMPLE_ERROR1), - sequenceNumber = SAMPLE_SEQUENCE_NUMBER - ) - val dataConnectResult2 = - DataConnectResult( - variables = SAMPLE_VARIABLES, - data = SAMPLE_DATA, - errors = listOf(SAMPLE_ERROR2), - sequenceNumber = SAMPLE_SEQUENCE_NUMBER - ) - assertThat(dataConnectResult1.hashCode()).isNotEqualTo(dataConnectResult2.hashCode()) - } -} + private data class TestVariables(val value: String) + + private val SAMPLE_VARIABLES = TestVariables("foo") -private data class TestVariables(val value: String) - -private val SAMPLE_VARIABLES = TestVariables("foo") - -private data class TestData(val value: String) - -private val SAMPLE_DATA = TestData("bar") - -private val SAMPLE_ERROR_MESSAGE1 = "This is a sample message" -private val SAMPLE_ERROR_PATH1 = listOf(PathSegment.Field("foo"), PathSegment.ListIndex(42)) -private val SAMPLE_ERROR_EXTENSIONS1 = mapOf("foo" to 42, "bar" to "BAR") -private val SAMPLE_ERROR_MESSAGE2 = "This is a sample message 2" -private val SAMPLE_ERROR_PATH2 = listOf(PathSegment.Field("bar"), PathSegment.ListIndex(24)) -private val SAMPLE_ERROR_EXTENSIONS2 = mapOf("blah" to 99, "zzz" to "foo") -private val SAMPLE_ERROR1 = - DataConnectError( - message = SAMPLE_ERROR_MESSAGE1, - path = SAMPLE_ERROR_PATH1, - extensions = SAMPLE_ERROR_EXTENSIONS1 - ) -private val SAMPLE_ERROR2 = - DataConnectError( - message = SAMPLE_ERROR_MESSAGE2, - path = SAMPLE_ERROR_PATH2, - extensions = SAMPLE_ERROR_EXTENSIONS2 - ) -private val SAMPLE_ERRORS = listOf(SAMPLE_ERROR1, SAMPLE_ERROR2) -private val SAMPLE_SEQUENCE_NUMBER: Long = -1 + private data class TestData(val value: String) + + private val SAMPLE_DATA = TestData("bar") + + private val SAMPLE_SEQUENCE_NUMBER: Long = -1 +} From 54c8837fe4b6898eccd803701fe14d1f25e3be53 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 4 Dec 2023 22:52:12 -0500 Subject: [PATCH 108/573] Untyped variables and data added (#520) --- .../testing_graphql_schemas/person/ops.gql | 4 + .../DataConnectUntypedDataIntegrationTest.kt | 373 ++++++++++++++++++ ...aConnectUntypedVariablesIntegrationTest.kt | 122 ++++++ .../testutil/schemas/PersonSchema.kt | 11 + .../dataconnect/DataConnectGrpcClient.kt | 5 +- .../dataconnect/DataConnectUntypedData.kt | 39 ++ .../DataConnectUntypedVariables.kt | 40 ++ .../dataconnect/FirebaseDataConnect.kt | 7 +- .../firebase/dataconnect/MutationRef.kt | 20 + .../google/firebase/dataconnect/ProtoUtil.kt | 102 +++++ .../firebase/dataconnect/QueryManager.kt | 7 +- .../google/firebase/dataconnect/QueryRef.kt | 20 + 12 files changed, 747 insertions(+), 3 deletions(-) create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/DataConnectUntypedDataIntegrationTest.kt create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/DataConnectUntypedVariablesIntegrationTest.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedVariables.kt diff --git a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql index eacad92255d..b08cb517cb0 100644 --- a/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql +++ b/firebase-dataconnect/src/androidTest/assets/testing_graphql_schemas/person/ops.gql @@ -16,6 +16,10 @@ mutation createPerson($data: Person_Data! @pick(fields: ["id", "name", "age"])) person_insert(data: $data) } +mutation createDefaultPerson @auth(is: PUBLIC) { + person_insert(data: {id: "DefaultId", name: "DefaultName", age: 42}) +} + mutation deletePerson($id: String!) @auth(is: PUBLIC) { person_delete(id: $id) } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/DataConnectUntypedDataIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/DataConnectUntypedDataIntegrationTest.kt new file mode 100644 index 00000000000..94a989d0b76 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/DataConnectUntypedDataIntegrationTest.kt @@ -0,0 +1,373 @@ +// Copyright 2023 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.dataconnect + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertWithMessage +import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule +import com.google.firebase.dataconnect.testutil.TestDataConnectFactory +import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DataConnectUntypedDataIntegrationTest { + + @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val dataConnectFactory = TestDataConnectFactory() + + private val allTypesSchema + get() = dataConnectFactory.allTypesSchema + + @Test + fun primitiveTypes() = runTest { + allTypesSchema.createPrimitive.execute( + AllTypesSchema.CreatePrimitiveMutation.Variables( + AllTypesSchema.PrimitiveData( + id = "abc123", + idFieldNullable = "xyz", + intField = 42, + intFieldNullable = 43, + floatField = 99.0, + floatFieldNullable = 100.0, + booleanField = false, + booleanFieldNullable = true, + stringField = "TestStringValue", + stringFieldNullable = "TestStringNullableValue", + ) + ) + ) + val query = allTypesSchema.getPrimitive.withDataDeserializer(DataConnectUntypedData) + + val result = query.execute(AllTypesSchema.GetPrimitiveQuery.Variables(id = "abc123")) + + assertWithMessage("errors").that(result.data.errors).isEmpty() + assertWithMessage("data").that(result.data.data).isNotNull() + assertWithMessage("data.keys").that(result.data.data?.keys).containsExactly("primitive") + assertWithMessage("data.keys[primitive]") + .that(result.data.data?.get("primitive") as Map<*, *>) + .containsExactlyEntriesIn( + mapOf( + "id" to "abc123", + "idFieldNullable" to "xyz", + "intField" to 42.0, + "intFieldNullable" to 43.0, + "floatField" to 99.0, + "floatFieldNullable" to 100.0, + "booleanField" to false, + "booleanFieldNullable" to true, + "stringField" to "TestStringValue", + "stringFieldNullable" to "TestStringNullableValue", + ) + ) + } + + @Test + fun nullPrimitiveTypes() = runTest { + allTypesSchema.createPrimitive.execute( + AllTypesSchema.CreatePrimitiveMutation.Variables( + AllTypesSchema.PrimitiveData( + id = "abc123", + idFieldNullable = null, + intField = 42, + intFieldNullable = null, + floatField = 99.0, + floatFieldNullable = null, + booleanField = false, + booleanFieldNullable = null, + stringField = "TestStringValue", + stringFieldNullable = null, + ) + ) + ) + val query = allTypesSchema.getPrimitive.withDataDeserializer(DataConnectUntypedData) + + val result = query.execute(AllTypesSchema.GetPrimitiveQuery.Variables(id = "abc123")) + + assertWithMessage("errors").that(result.data.errors).isEmpty() + assertWithMessage("data").that(result.data.data).isNotNull() + assertWithMessage("data.keys").that(result.data.data?.keys).containsExactly("primitive") + assertWithMessage("data.keys[primitive]") + .that(result.data.data?.get("primitive") as Map<*, *>) + .containsExactlyEntriesIn( + mapOf( + "id" to "abc123", + "idFieldNullable" to null, + "intField" to 42.0, + "intFieldNullable" to null, + "floatField" to 99.0, + "floatFieldNullable" to null, + "booleanField" to false, + "booleanFieldNullable" to null, + "stringField" to "TestStringValue", + "stringFieldNullable" to null, + ) + ) + } + + @Test + fun listsOfPrimitiveTypes() = runTest { + allTypesSchema.createPrimitiveList.execute( + AllTypesSchema.CreatePrimitiveListMutation.Variables( + AllTypesSchema.PrimitiveListData( + id = "abc123", + idListNullable = listOf("aaa", "bbb"), + idListOfNullable = listOf("ccc", "ddd"), + intList = listOf(42, 43, 44), + intListNullable = listOf(45, 46), + intListOfNullable = listOf(47, 48), + floatList = listOf(12.3, 45.6, 78.9), + floatListNullable = listOf(98.7, 65.4), + floatListOfNullable = listOf(100.1, 100.2), + booleanList = listOf(true, false, true, false), + booleanListNullable = listOf(false, true, false, true), + booleanListOfNullable = listOf(false, false, true, true), + stringList = listOf("xxx", "yyy", "zzz"), + stringListNullable = listOf("qqq", "rrr"), + stringListOfNullable = listOf("sss", "ttt"), + ) + ) + ) + val query = allTypesSchema.getPrimitiveList.withDataDeserializer(DataConnectUntypedData) + + val result = query.execute(AllTypesSchema.GetPrimitiveListQuery.Variables(id = "abc123")) + + assertWithMessage("errors").that(result.data.errors).isEmpty() + assertWithMessage("data").that(result.data.data).isNotNull() + assertWithMessage("data.keys").that(result.data.data?.keys).containsExactly("primitiveList") + assertWithMessage("data.keys[primitiveList]") + .that(result.data.data?.get("primitiveList") as Map<*, *>) + .containsExactlyEntriesIn( + mapOf( + "id" to "abc123", + "idListNullable" to listOf("aaa", "bbb"), + "idListOfNullable" to listOf("ccc", "ddd"), + "intList" to listOf(42.0, 43.0, 44.0), + "intListNullable" to listOf(45.0, 46.0), + "intListOfNullable" to listOf(47.0, 48.0), + "floatList" to listOf(12.3, 45.6, 78.9), + "floatListNullable" to listOf(98.7, 65.4), + "floatListOfNullable" to listOf(100.1, 100.2), + "booleanList" to listOf(true, false, true, false), + "booleanListNullable" to listOf(false, true, false, true), + "booleanListOfNullable" to listOf(false, false, true, true), + "stringList" to listOf("xxx", "yyy", "zzz"), + "stringListNullable" to listOf("qqq", "rrr"), + "stringListOfNullable" to listOf("sss", "ttt"), + ) + ) + } + + @Test + fun nullListsOfPrimitiveTypes() = runTest { + allTypesSchema.createPrimitiveList.execute( + AllTypesSchema.CreatePrimitiveListMutation.Variables( + AllTypesSchema.PrimitiveListData( + id = "abc123", + idListNullable = null, + idListOfNullable = listOf("ccc", "ddd"), + intList = listOf(42, 43, 44), + intListNullable = null, + intListOfNullable = listOf(47, 48), + floatList = listOf(12.3, 45.6, 78.9), + floatListNullable = null, + floatListOfNullable = listOf(100.1, 100.2), + booleanList = listOf(true, false, true, false), + booleanListNullable = null, + booleanListOfNullable = listOf(false, false, true, true), + stringList = listOf("xxx", "yyy", "zzz"), + stringListNullable = null, + stringListOfNullable = listOf("sss", "ttt"), + ) + ) + ) + val query = allTypesSchema.getPrimitiveList.withDataDeserializer(DataConnectUntypedData) + + val result = query.execute(AllTypesSchema.GetPrimitiveListQuery.Variables(id = "abc123")) + + assertWithMessage("errors").that(result.data.errors).isEmpty() + assertWithMessage("data").that(result.data.data).isNotNull() + assertWithMessage("data.keys").that(result.data.data?.keys).containsExactly("primitiveList") + assertWithMessage("data.keys[primitiveList]") + .that(result.data.data?.get("primitiveList") as Map<*, *>) + .containsExactlyEntriesIn( + mapOf( + "id" to "abc123", + "idListNullable" to null, + "idListOfNullable" to listOf("ccc", "ddd"), + "intList" to listOf(42.0, 43.0, 44.0), + "intListNullable" to null, + "intListOfNullable" to listOf(47.0, 48.0), + "floatList" to listOf(12.3, 45.6, 78.9), + "floatListNullable" to null, + "floatListOfNullable" to listOf(100.1, 100.2), + "booleanList" to listOf(true, false, true, false), + "booleanListNullable" to null, + "booleanListOfNullable" to listOf(false, false, true, true), + "stringList" to listOf("xxx", "yyy", "zzz"), + "stringListNullable" to null, + "stringListOfNullable" to listOf("sss", "ttt"), + ) + ) + } + + @Test + fun nestedStructs() = runTest { + allTypesSchema.createFarmer(id = "Farmer1Id", name = "Farmer1Name", parentId = null) + allTypesSchema.createFarmer(id = "Farmer2Id", name = "Farmer2Name", parentId = "Farmer1Id") + allTypesSchema.createFarmer(id = "Farmer3Id", name = "Farmer3Name", parentId = "Farmer2Id") + allTypesSchema.createFarmer(id = "Farmer4Id", name = "Farmer4Name", parentId = "Farmer3Id") + allTypesSchema.createFarm(id = "FarmId", name = "TestFarm", farmerId = "Farmer4Id") + allTypesSchema.createAnimal( + id = "Animal1Id", + farmId = "FarmId", + name = "Animal1Name", + species = "Animal1Species", + age = 1 + ) + allTypesSchema.createAnimal( + id = "Animal2Id", + farmId = "FarmId", + name = "Animal2Name", + species = "Animal2Species", + age = null + ) + val query = allTypesSchema.getFarm.withDataDeserializer(DataConnectUntypedData) + + val result = query.execute(AllTypesSchema.GetFarmQuery.Variables(id = "FarmId")) + + assertWithMessage("errors").that(result.data.errors).isEmpty() + assertWithMessage("data").that(result.data.data).isNotNull() + assertWithMessage("data.keys").that(result.data.data?.keys).containsExactly("farm") + val farm = + result.data.data!!.get("farm").let { + val farm = it as? Map<*, *> + assertWithMessage("farm: $it").that(farm).isNotNull() + farm!! + } + assertWithMessage("farm.keys") + .that(farm.keys) + .containsExactly("id", "name", "farmer", "animals") + assertWithMessage("farm[id]").that(farm["id"]).isEqualTo("FarmId") + assertWithMessage("farm[name]").that(farm["name"]).isEqualTo("TestFarm") + val animals = + farm["animals"].let { + val animals = it as? List<*> + assertWithMessage("animals: $it").that(animals).isNotNull() + animals!! + } + assertWithMessage("farm[animals]") + .that(animals) + .containsExactly( + mapOf( + "id" to "Animal1Id", + "name" to "Animal1Name", + "species" to "Animal1Species", + "age" to 1.0 + ), + mapOf( + "id" to "Animal2Id", + "name" to "Animal2Name", + "species" to "Animal2Species", + "age" to null + ), + ) + val farmer = + farm["farmer"].let { + val farmer = it as? Map<*, *> + assertWithMessage("farmer: $it").that(farmer).isNotNull() + farmer!! + } + assertWithMessage("farmer.keys").that(farmer.keys).containsExactly("id", "name", "parent") + assertWithMessage("farmer[id]").that(farmer["id"]).isEqualTo("Farmer4Id") + assertWithMessage("farmer[name]").that(farmer["name"]).isEqualTo("Farmer4Name") + val parent = + farmer["parent"].let { + val parent = it as? Map<*, *> + assertWithMessage("parent: $it").that(parent).isNotNull() + parent!! + } + assertWithMessage("parent.keys").that(parent.keys).containsExactly("id", "name", "parentId") + assertWithMessage("parent[id]").that(parent["id"]).isEqualTo("Farmer3Id") + assertWithMessage("parent[name]").that(parent["name"]).isEqualTo("Farmer3Name") + assertWithMessage("parent[parentId]").that(parent["parentId"]).isEqualTo("Farmer2Id") + } + + @Test + fun nestedNullStructs() = runTest { + allTypesSchema.createFarmer(id = "FarmerId", name = "FarmerName", parentId = null) + allTypesSchema.createFarm(id = "FarmId", name = "TestFarm", farmerId = "FarmerId") + val query = allTypesSchema.getFarm.withDataDeserializer(DataConnectUntypedData) + + val result = query.execute(AllTypesSchema.GetFarmQuery.Variables(id = "FarmId")) + + assertWithMessage("errors").that(result.data.errors).isEmpty() + assertWithMessage("data").that(result.data.data).isNotNull() + assertWithMessage("data.keys").that(result.data.data?.keys).containsExactly("farm") + val farm = + result.data.data!!.get("farm").let { + val farm = it as? Map<*, *> + assertWithMessage("farm: $it").that(farm).isNotNull() + farm!! + } + assertWithMessage("farm.keys") + .that(farm.keys) + .containsExactly("id", "name", "farmer", "animals") + val farmer = + farm["farmer"].let { + val farmer = it as? Map<*, *> + assertWithMessage("farmer: $it").that(farmer).isNotNull() + farmer!! + } + assertWithMessage("farmer.keys").that(farmer.keys).containsExactly("id", "name", "parent") + assertWithMessage("farmer[id]").that(farmer["id"]).isEqualTo("FarmerId") + assertWithMessage("farmer[name]").that(farmer["name"]).isEqualTo("FarmerName") + assertWithMessage("farmer[parent]").that(farmer["parent"]).isNull() + } + + @Test + fun queryErrorsReturnedByServerArePutInTheErrorsListInsteadOfThrowingAnException() = runTest { + @Serializable data class BogusVariables(val foo: String) + val query = + allTypesSchema.getPrimitive + .withVariablesSerializer(serializer()) + .withDataDeserializer(DataConnectUntypedData) + + val result = query.execute(BogusVariables(foo = "bar")) + + assertWithMessage("result.data.data").that(result.data.data).isNull() + assertWithMessage("result.data.errors").that(result.data.errors).hasSize(1) + } + + @Test + fun mutationErrorsReturnedByServerArePutInTheErrorsListInsteadOfThrowingAnException() = runTest { + @Serializable data class BogusVariables(val foo: String) + val mutation = + allTypesSchema.createAnimal + .withVariablesSerializer(serializer()) + .withDataDeserializer(DataConnectUntypedData) + + val result = mutation.execute(BogusVariables(foo = "bar")) + + assertWithMessage("result.data.data").that(result.data.data).isNull() + assertWithMessage("result.data.errors").that(result.data.errors).hasSize(1) + } +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/DataConnectUntypedVariablesIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/DataConnectUntypedVariablesIntegrationTest.kt new file mode 100644 index 00000000000..97f14eb0606 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/DataConnectUntypedVariablesIntegrationTest.kt @@ -0,0 +1,122 @@ +// Copyright 2023 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.dataconnect + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule +import com.google.firebase.dataconnect.testutil.TestDataConnectFactory +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreateDefaultPersonMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation.execute +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.execute +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.* +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DataConnectUntypedVariablesIntegrationTest { + + @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val dataConnectFactory = TestDataConnectFactory() + + private val personSchema + get() = dataConnectFactory.personSchema + + @Test + fun emptyMapWorksWithQuery() = runTest { + personSchema.createPerson.execute(id = "Person1Id", name = "Person1Name", age = 42) + personSchema.createPerson.execute(id = "Person2Id", name = "Person2Name", age = 43) + val query = personSchema.getAllPeople.withVariablesSerializer(DataConnectUntypedVariables) + + val result = query.execute(DataConnectUntypedVariables()) + + assertThat(result.variables).isEqualTo(DataConnectUntypedVariables()) + assertThat(result.data) + .isEqualTo( + PersonSchema.GetAllPeopleQuery.Data( + people = + listOf( + PersonSchema.GetAllPeopleQuery.Data.Person( + id = "Person1Id", + name = "Person1Name", + age = 42 + ), + PersonSchema.GetAllPeopleQuery.Data.Person( + id = "Person2Id", + name = "Person2Name", + age = 43 + ), + ) + ) + ) + } + + @Test + fun nonEmptyMapWorksWithQuery() = runTest { + personSchema.createPerson.execute(id = "Person1Id", name = "Person1Name", age = 42) + personSchema.createPerson.execute(id = "Person2Id", name = "Person2Name", age = 43) + personSchema.createPerson.execute(id = "Person3Id", name = "Person3Name", age = null) + val query = personSchema.getPerson.withVariablesSerializer(DataConnectUntypedVariables) + + val result = query.execute(DataConnectUntypedVariables("id" to "Person2Id")) + + assertThat(result.variables).isEqualTo(DataConnectUntypedVariables("id" to "Person2Id")) + assertThat(result.data) + .isEqualTo( + PersonSchema.GetPersonQuery.Data( + person = PersonSchema.GetPersonQuery.Data.Person(name = "Person2Name", age = 43) + ) + ) + } + + @Test + fun emptyMapWorksWithMutation() = runTest { + val mutation = + personSchema.createDefaultPerson.withVariablesSerializer(DataConnectUntypedVariables) + + mutation.execute(DataConnectUntypedVariables()) + + val result = personSchema.getPerson.execute(id = "DefaultId") + assertThat(result.data) + .isEqualTo( + PersonSchema.GetPersonQuery.Data( + PersonSchema.GetPersonQuery.Data.Person(name = "DefaultName", age = 42) + ) + ) + } + + @Test + fun nonEmptyMapWorksWithMutation() = runTest { + val mutation = personSchema.createPerson.withVariablesSerializer(DataConnectUntypedVariables) + + mutation.execute( + DataConnectUntypedVariables( + "data" to mapOf("id" to "PersonId", "name" to "TestPersonName", "age" to 42.0) + ) + ) + + val result = personSchema.getPerson.execute(id = "PersonId") + assertThat(result.data) + .isEqualTo( + PersonSchema.GetPersonQuery.Data( + PersonSchema.GetPersonQuery.Data.Person(name = "TestPersonName", age = 42) + ) + ) + } +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index bd6b7ab154f..8f908277047 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -38,6 +38,17 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { dataConnect.installEmulatorSchema("testing_graphql_schemas/person") } + object CreateDefaultPersonMutation { + suspend fun MutationRef.execute() = execute(Unit) + } + + val createDefaultPerson = + dataConnect.mutation( + operationName = "createDefaultPerson", + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + object CreatePersonMutation { @Serializable data class PersonData(val id: String, val name: String, val age: Int? = null) @Serializable data class Variables(val data: PersonData) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 0ee94c1ae70..2be1bbd378f 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -249,7 +249,10 @@ internal fun DataConnectGrpcClient.OperationResult.deserialize( dataDeserializer: DeserializationStrategy ): DataConnectGrpcClient.DeserialzedOperationResult { val deserializedData: T = - if (data === null) { + if (dataDeserializer === DataConnectUntypedData) { + @Suppress("UNCHECKED_CAST") + DataConnectUntypedData(data?.toMap(), errors) as T + } else if (data === null) { // TODO: include the variables and error list in the thrown exception throw DataConnectException("no data included in result: errors=$errors") } else if (errors.isNotEmpty()) { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt new file mode 100644 index 00000000000..65ccc49773d --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt @@ -0,0 +1,39 @@ +// Copyright 2023 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.dataconnect + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder + +class DataConnectUntypedData +constructor(val data: Map?, val errors: List) { + + override fun equals(other: Any?) = + (other as? DataConnectUntypedData)?.let { it.data == data && it.errors == errors } ?: false + override fun hashCode() = (data?.hashCode() ?: 0) * 31 * errors.hashCode() + override fun toString() = "DataConnectUntypedData(data=$data, errors=$errors)" + + companion object Deserializer : DeserializationStrategy { + override val descriptor: SerialDescriptor + get() = unsupported() + + override fun deserialize(decoder: Decoder): DataConnectUntypedData = unsupported() + + private fun unsupported(): Nothing = + throw UnsupportedOperationException( + "this DeserializationStrategy cannot actually be used; it is merely a placeholder" + ) + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedVariables.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedVariables.kt new file mode 100644 index 00000000000..da052bd7645 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedVariables.kt @@ -0,0 +1,40 @@ +// Copyright 2023 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.dataconnect + +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Encoder + +class DataConnectUntypedVariables constructor(val variables: Map) { + constructor(vararg pairs: Pair) : this(mapOf(*pairs)) + constructor(builderAction: MutableMap.() -> Unit) : this(buildMap(builderAction)) + + override fun equals(other: Any?) = + (other as? DataConnectUntypedVariables)?.let { it.variables == variables } ?: false + override fun hashCode() = variables.hashCode() + override fun toString() = variables.toString() + + companion object Serializer : SerializationStrategy { + override val descriptor: SerialDescriptor + get() = unsupported() + + override fun serialize(encoder: Encoder, value: DataConnectUntypedVariables) = unsupported() + + private fun unsupported(): Nothing = + throw UnsupportedOperationException( + "this SerializationStrategy cannot actually be used; it is merely a placeholder" + ) + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 1a8854cbc5d..cde5eb3afe8 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -120,7 +120,12 @@ internal constructor( requestId = requestId, sequenceNumber = nextSequenceNumber(), operationName = ref.operationName, - variables = encodeToStruct(ref.variablesSerializer, variables) + variables = + if (ref.variablesSerializer === DataConnectUntypedVariables.Serializer) + (variables as DataConnectUntypedVariables).variables.toStructProto() + else { + encodeToStruct(ref.variablesSerializer, variables) + } ) .runCatching { withContext(blockingDispatcher) { deserialize(ref.dataDeserializer) } } .onFailure { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt index 0a779cc32c9..083ddc2b6e4 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/MutationRef.kt @@ -32,4 +32,24 @@ internal constructor( override suspend fun execute( variables: VariablesType ): DataConnectResult = dataConnect.executeMutation(this, variables) + + fun withDataDeserializer( + newDataDeserializer: DeserializationStrategy + ): MutationRef = + MutationRef( + dataConnect = dataConnect, + operationName = operationName, + variablesSerializer = variablesSerializer, + dataDeserializer = newDataDeserializer + ) + + fun withVariablesSerializer( + newVariablesSerializer: SerializationStrategy + ): MutationRef = + MutationRef( + dataConnect = dataConnect, + operationName = operationName, + variablesSerializer = newVariablesSerializer, + dataDeserializer = dataDeserializer + ) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt index 89f0ef51140..7594c96c9b3 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt @@ -302,3 +302,105 @@ internal fun ExecuteMutationResponse.toCompactString(): String = putList("errors") { errorsList.forEach { add(it.toDataConnectError().toString()) } } } .toCompactString() + +internal fun Struct.toMap(): Map { + val mutualRecursion = + object { + val listForListValue: DeepRecursiveFunction> = DeepRecursiveFunction { + buildList { + it.valuesList.forEach { value -> + add( + when (val kind = value.kindCase) { + KindCase.NULL_VALUE -> null + KindCase.BOOL_VALUE -> value.boolValue + KindCase.NUMBER_VALUE -> value.numberValue + KindCase.STRING_VALUE -> value.stringValue + KindCase.LIST_VALUE -> callRecursive(value.listValue) + KindCase.STRUCT_VALUE -> mapForStruct.callRecursive(value.structValue) + else -> throw IllegalArgumentException("unsupported kind: $kind") + } + ) + } + } + } + + val mapForStruct: DeepRecursiveFunction> = DeepRecursiveFunction { + buildMap { + it.fieldsMap.entries.forEach { (key, value) -> + put( + key, + when (val kind = value.kindCase) { + KindCase.NULL_VALUE -> null + KindCase.BOOL_VALUE -> value.boolValue + KindCase.NUMBER_VALUE -> value.numberValue + KindCase.STRING_VALUE -> value.stringValue + KindCase.LIST_VALUE -> listForListValue.callRecursive(value.listValue) + KindCase.STRUCT_VALUE -> callRecursive(value.structValue) + else -> throw IllegalArgumentException("unsupported kind: $kind") + } + ) + } + } + } + } + + return mutualRecursion.mapForStruct(this) +} + +internal fun Map.toStructProto(): Struct { + val mutualRecursion = + object { + val listValueForList: DeepRecursiveFunction, ListValue> = DeepRecursiveFunction { + val listValueProtoBuilder = ListValue.newBuilder() + it.forEach { value -> + listValueProtoBuilder.addValues( + when (value) { + null -> nullProtoValue + is Boolean -> value.toValueProto() + is Double -> value.toValueProto() + is String -> value.toValueProto() + is List<*> -> callRecursive(value).toValueProto() + is Map<*, *> -> structForMap.callRecursive(value).toValueProto() + else -> + throw IllegalArgumentException( + "unsupported type: ${value::class.qualifiedName}; " + + "supported types are: Boolean, Double, String, List, and Map" + ) + } + ) + } + listValueProtoBuilder.build() + } + + val structForMap: DeepRecursiveFunction, Struct> = DeepRecursiveFunction { + val structProtoBuilder = Struct.newBuilder() + it.entries.forEach { (untypedKey, value) -> + val key = + (untypedKey as? String) + ?: throw IllegalArgumentException( + "map keys must be string, but got: " + + if (untypedKey === null) "null" else untypedKey::class.qualifiedName + ) + structProtoBuilder.putFields( + key, + when (value) { + null -> nullProtoValue + is Double -> value.toValueProto() + is Boolean -> value.toValueProto() + is String -> value.toValueProto() + is List<*> -> listValueForList.callRecursive(value).toValueProto() + is Map<*, *> -> callRecursive(value).toValueProto() + else -> + throw IllegalArgumentException( + "unsupported type: ${value::class.qualifiedName}; " + + "supported types are: Boolean, Double, String, List, and Map" + ) + } + ) + } + structProtoBuilder.build() + } + } + + return mutualRecursion.structForMap(this) +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index c383bf82a68..088cbe29a0e 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -316,7 +316,12 @@ private class QueryStates( // NOTE: This function MUST be called from a coroutine that has locked `mutex`. private fun acquireQueryState(ref: QueryRef, variables: V): QueryState { - val variablesStruct = encodeToStruct(ref.variablesSerializer, variables) + val variablesStruct = + if (ref.variablesSerializer === DataConnectUntypedVariables.Serializer) { + (variables as DataConnectUntypedVariables).variables.toStructProto() + } else { + encodeToStruct(ref.variablesSerializer, variables) + } val variablesHash = variablesStruct.calculateSha512().toAlphaNumericString() val key = QueryStateKey(operationName = ref.operationName, variablesHash = variablesHash) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt index ec597c04a00..352e8900854 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt @@ -36,4 +36,24 @@ internal constructor( fun subscribe(variables: VariablesType): QuerySubscription = QuerySubscription(this, variables) + + fun withDataDeserializer( + newDataDeserializer: DeserializationStrategy + ): QueryRef = + QueryRef( + dataConnect = dataConnect, + operationName = operationName, + variablesSerializer = variablesSerializer, + dataDeserializer = newDataDeserializer + ) + + fun withVariablesSerializer( + newVariablesSerializer: SerializationStrategy + ): QueryRef = + QueryRef( + dataConnect = dataConnect, + operationName = operationName, + variablesSerializer = newVariablesSerializer, + dataDeserializer = dataDeserializer + ) } From 47e0128b41728bfab5e153308d56600e6eb7428d Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 5 Dec 2023 13:03:43 -0500 Subject: [PATCH 109/573] Mark decodeFromStruct() and encodeToStruct() as 'internal' instead of 'public' --- .../com/google/firebase/dataconnect/ProtoStructDecoder.kt | 5 +++-- .../com/google/firebase/dataconnect/ProtoStructEncoder.kt | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt index c684a200aa6..ce1eed8c1a4 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructDecoder.kt @@ -18,9 +18,10 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer -inline fun decodeFromStruct(struct: Struct): T = decodeFromStruct(serializer(), struct) +internal inline fun decodeFromStruct(struct: Struct): T = + decodeFromStruct(serializer(), struct) -fun decodeFromStruct(deserializer: DeserializationStrategy, struct: Struct): T { +internal fun decodeFromStruct(deserializer: DeserializationStrategy, struct: Struct): T { val protoValue = Value.newBuilder().setStructValue(struct).build() return ProtoValueDecoder(protoValue, path = null).decodeSerializableValue(deserializer) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt index 3d1be8ab2ff..b4eb6dd03ba 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoStructEncoder.kt @@ -16,9 +16,10 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer -inline fun encodeToStruct(value: T): Struct = encodeToStruct(serializer(), value) +internal inline fun encodeToStruct(value: T): Struct = + encodeToStruct(serializer(), value) -fun encodeToStruct(serializer: SerializationStrategy, value: T): Struct { +internal fun encodeToStruct(serializer: SerializationStrategy, value: T): Struct { val values = mutableListOf() ProtoValueEncoder(path = null, onValue = values::add).encodeSerializableValue(serializer, value) if (values.isEmpty()) { From e15df420f0eef252dc5d0169cc2206db696a6b21 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 5 Dec 2023 16:02:43 -0500 Subject: [PATCH 110/573] QueryManager.kt: rename "QueryState" to "LiveQuery" --- .../firebase/dataconnect/QueryManager.kt | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 088cbe29a0e..c6e13b157b6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -32,12 +32,12 @@ internal class QueryManager( ) { private val logger = Logger("QueryManager").apply { debug { "Created from $creatorLoggerId" } } - private val queryStates = - QueryStates(grpcClient, coroutineScope, deserializationDispatcher, logger) + private val liveQueries = + LiveQueries(grpcClient, coroutineScope, deserializationDispatcher, logger) suspend fun execute(ref: QueryRef, variables: V): DataConnectResult = - queryStates - .withQueryState(ref, variables) { it.execute(ref.dataDeserializer) } + liveQueries + .withLiveQuery(ref, variables) { it.execute(ref.dataDeserializer) } .toDataConnectResult(variables) suspend fun onResult( @@ -47,8 +47,8 @@ internal class QueryManager( executeQuery: Boolean, callback: suspend (DataConnectResult) -> Unit, ): Nothing = - queryStates.withQueryState(ref, variables) { queryState -> - queryState.onResult( + liveQueries.withLiveQuery(ref, variables) { liveQuery -> + liveQuery.onResult( ref.dataDeserializer, sinceSequenceNumber = sinceSequenceNumber, executeQuery = executeQuery @@ -58,10 +58,8 @@ internal class QueryManager( } } -private data class QueryStateKey(val operationName: String, val variablesHash: String) - -private class QueryState( - val key: QueryStateKey, +private class LiveQuery( + val key: Key, private val grpcClient: DataConnectGrpcClient, private val operationName: String, private val variables: Struct, @@ -189,6 +187,8 @@ private class QueryState( dataDeserializers.add(it) } } + + data class Key(val operationName: String, val variablesHash: String) } private class RegisteredDataDeserialzer( @@ -286,7 +286,7 @@ private class RegisteredDataDeserialzer( } } -private class QueryStates( +private class LiveQueries( private val grpcClient: DataConnectGrpcClient, private val coroutineScope: CoroutineScope, private val deserializationDispatcher: CoroutineDispatcher, @@ -294,28 +294,28 @@ private class QueryStates( ) { private val mutex = Mutex() - // NOTE: All accesses to `referenceCountedQueryStateByKey` and the `refCount` field of each value + // NOTE: All accesses to `referenceCountedLiveQueryByKey` and the `refCount` field of each value // MUST be done from a coroutine that has locked `mutex`; otherwise, such accesses (both reads and // writes) are data races and yield undefined behavior. - private val referenceCountedQueryStateByKey = - mutableMapOf>() + private val referenceCountedLiveQueryByKey = + mutableMapOf>() - suspend fun withQueryState( + suspend fun withLiveQuery( ref: QueryRef, variables: V, - block: suspend (QueryState) -> R + block: suspend (LiveQuery) -> R ): R { - val queryState = mutex.withLock { acquireQueryState(ref, variables) } + val liveQuery = mutex.withLock { acquireLiveQuery(ref, variables) } return try { - block(queryState) + block(liveQuery) } finally { - mutex.withLock { withContext(NonCancellable) { releaseQueryState(queryState) } } + mutex.withLock { withContext(NonCancellable) { releaseLiveQuery(liveQuery) } } } } // NOTE: This function MUST be called from a coroutine that has locked `mutex`. - private fun acquireQueryState(ref: QueryRef, variables: V): QueryState { + private fun acquireLiveQuery(ref: QueryRef, variables: V): LiveQuery { val variablesStruct = if (ref.variablesSerializer === DataConnectUntypedVariables.Serializer) { (variables as DataConnectUntypedVariables).variables.toStructProto() @@ -323,12 +323,12 @@ private class QueryStates( encodeToStruct(ref.variablesSerializer, variables) } val variablesHash = variablesStruct.calculateSha512().toAlphaNumericString() - val key = QueryStateKey(operationName = ref.operationName, variablesHash = variablesHash) + val key = LiveQuery.Key(operationName = ref.operationName, variablesHash = variablesHash) - val referenceCountedQueryState = - referenceCountedQueryStateByKey.getOrPut(key) { + val referenceCountedLiveQuery = + referenceCountedLiveQueryByKey.getOrPut(key) { ReferenceCounted( - QueryState( + LiveQuery( key = key, grpcClient = grpcClient, operationName = ref.operationName, @@ -341,24 +341,24 @@ private class QueryStates( ) } - referenceCountedQueryState.refCount++ + referenceCountedLiveQuery.refCount++ - return referenceCountedQueryState.obj + return referenceCountedLiveQuery.obj } // NOTE: This function MUST be called from a coroutine that has locked `mutex`. - private fun releaseQueryState(queryState: QueryState) { - val referenceCountedQueryState = referenceCountedQueryStateByKey[queryState.key] + private fun releaseLiveQuery(liveQuery: LiveQuery) { + val referenceCountedLiveQuery = referenceCountedLiveQueryByKey[liveQuery.key] - if (referenceCountedQueryState === null) { - error("unexpected null QueryState for key: ${queryState.key}") - } else if (referenceCountedQueryState.obj !== queryState) { - error("unexpected QueryState for key: ${queryState.key}: ${referenceCountedQueryState.obj}") + if (referenceCountedLiveQuery === null) { + error("unexpected null LiveQuery for key: ${liveQuery.key}") + } else if (referenceCountedLiveQuery.obj !== liveQuery) { + error("unexpected LiveQuery for key: ${liveQuery.key}: ${referenceCountedLiveQuery.obj}") } - referenceCountedQueryState.refCount-- - if (referenceCountedQueryState.refCount == 0) { - referenceCountedQueryStateByKey.remove(queryState.key) + referenceCountedLiveQuery.refCount-- + if (referenceCountedLiveQuery.refCount == 0) { + referenceCountedLiveQueryByKey.remove(liveQuery.key) } } } From 586474d5e92ce484e49072cf1f791976931a4fee Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 5 Dec 2023 22:12:29 -0500 Subject: [PATCH 111/573] SynchronizedLazy added, and used by QueryManager. --- .../firebase/dataconnect/QueryManager.kt | 38 ++++++------------ .../com/google/firebase/dataconnect/Util.kt | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index c6e13b157b6..767acd29965 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -83,7 +83,7 @@ private class LiveQuery( // Register the data deserialzier _before_ waiting for the current job to complete. This // guarantees that the deserializer will be registered by the time the subsequent job (`newJob` // below) runs. - val registeredDataDeserialzer = + val registeredDataDeserializer = registerDataDeserializer(dataDeserializer, deserializationDispatcher) // Wait for the current job to complete (if any), and ignore its result. Waiting avoids running @@ -107,7 +107,7 @@ private class LiveQuery( newJob.join() - return registeredDataDeserialzer.getLatestUpdate()!!.getOrThrow() + return registeredDataDeserializer.getLatestUpdate()!!.getOrThrow() } suspend fun onResult( @@ -201,7 +201,7 @@ private class RegisteredDataDeserialzer( data class Update( val sequenceNumber: Long, - val result: Lazy>> + val result: SuspendingLazy>> ) fun update(requestId: String, sequenceNumber: Long, result: Result) { @@ -219,12 +219,8 @@ private class RegisteredDataDeserialzer( } } - suspend fun getLatestUpdate(): Result>? { - val lazyResult = latestUpdate.value?.result ?: return null - return if (lazyResult.isInitialized()) { - lazyResult.value - } else withContext(deserializationDispatcher) { lazyResult.value } - } + suspend fun getLatestUpdate(): Result>? = + latestUpdate.value?.result?.getValue() suspend fun getLatestSuccessfulUpdate(): DeserialzedOperationResult? { // Call getLatestUpdate() to populate `latestSuccessfulUpdate` with the most recent update. @@ -237,15 +233,11 @@ private class RegisteredDataDeserialzer( callback: suspend (DeserialzedOperationResult) -> Unit ): Nothing { var lastSequenceNumber = sinceSequenceNumber ?: Long.MIN_VALUE - latestUpdate.collect { - if (it !== null && lastSequenceNumber < it.sequenceNumber) { - val update = - if (it.result.isInitialized()) { - it.result.value - } else withContext(deserializationDispatcher) { it.result.value } - update.onSuccess { - lastSequenceNumber = it.sequenceNumber - callback(it) + latestUpdate.collect { update -> + if (update !== null && lastSequenceNumber < update.sequenceNumber) { + update.result.getValue().onSuccess { deserializedOperationResult -> + lastSequenceNumber = deserializedOperationResult.sequenceNumber + callback(deserializedOperationResult) } } } @@ -254,15 +246,9 @@ private class RegisteredDataDeserialzer( private fun lazyDeserialize( requestId: String, result: Result - ): Lazy>> = lazy { + ): SuspendingLazy>> = SuspendingLazy { result - .mapCatching { - // TODO: move the deserialization to a different dispatcher because it could take a decent - // amount of time. As a general rule of thumb, suspend functions shouldn't perform blocking - // I/O or long-running CPU-bound operations on the calling thread since the calling thread - // could be the main thread. - it.deserialize(deserializer) - } + .mapCatching { withContext(deserializationDispatcher) { it.deserialize(deserializer) } } .onFailure { // If the overall result was successful then the failure _must_ have occurred during // deserialization. Log the deserialization failure so it doesn't go unnoticed. diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 3adcc12e17a..9664ccffff6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -17,6 +17,8 @@ import java.io.OutputStream import java.util.concurrent.atomic.AtomicLong import kotlin.math.abs import kotlin.random.Random +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock internal object NullOutputStream : OutputStream() { override fun write(b: Int) {} @@ -113,3 +115,41 @@ internal fun ByteArray.toAlphaNumericString(): String = buildString { append(ALPHANUMERIC_ALPHABET[intValue and 0x1f]) } } + +/** + * An adaptation of the standard library [lazy] builder that implements + * [LazyThreadSafetyMode.SYNCHRONIZED] with a suspending function and a [Mutex] rather than a + * blocking synchronization call. + * + * @param initializer the block to invoke at most once to initialize this object's value. + */ +internal class SuspendingLazy(initializer: suspend () -> T) { + private val mutex = Mutex() + private var initializer: (suspend () -> T)? = initializer + @Volatile private var value: Any? = UninitializedValue + + val isInitialized: Boolean = value !== UninitializedValue + + suspend fun initialize() { + if (value === UninitializedValue) { + mutex.withLock { + if (value === UninitializedValue) { + value = initializer!!() + initializer = null + } + } + } + } + + suspend fun getValue(): T { + initialize() + + @Suppress("UNCHECKED_CAST") return value as T + } + + override fun toString(): String = + if (isInitialized) value.toString() else "SuspendingLazy value not initialized yet." + + // A sentinel value to use to indicate that the value is not yet initialized. + private companion object UninitializedValue +} From 48baba78e35894643b9f720ba371a2f81e8a494c Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 6 Dec 2023 10:20:39 -0500 Subject: [PATCH 112/573] QuerySubscriptionIntegrationTest.kt: clean up reload_concurrent_invocations_get_conflated --- .../QuerySubscriptionIntegrationTest.kt | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt index f2c7e44c204..a4152274f6f 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt @@ -157,9 +157,12 @@ class QuerySubscriptionIntegrationTest { schema.createPerson.execute(id = "TestId12345", name = "Name", age = 10000) val querySubscription = schema.getPerson.subscribe(id = "TestId12345") + val resultCollected = MutableStateFlow(false) val collectedResults = CopyOnWriteArrayList>() - backgroundScope.launch { querySubscription.resultFlow.toList(collectedResults) } + backgroundScope.launch { + querySubscription.resultFlow.onEach { resultCollected.value = true }.toList(collectedResults) + } val deferreds = buildList { repeat(25_000) { @@ -172,19 +175,24 @@ class QuerySubscriptionIntegrationTest { } // Wait for at least one result to come in. - while (collectedResults.isEmpty()) { - yield() - } + resultCollected.filter { it }.first() // Wait for all calls to reload() to complete. deferreds.forEach { it.await() } // Verify that we got the expected results. - collectedResults.forEachIndexed { index, result -> - assertWithMessage("collectedResults[$index]") - .that(result.data.person) + var collectedResultsIndex = 0 + while (collectedResultsIndex < collectedResults.size) { + assertWithMessage("collectedResults[$collectedResultsIndex]") + .that(collectedResults[collectedResultsIndex].data.person) .isEqualToGetPersonQueryResult(name = "Name", age = 10000) + collectedResultsIndex++ + yield() } + + // Verify that the calls to reload() were conflated, by ensuring that we got WAY less than + // 25,000 results. + assertWithMessage("collectedResultsIndex").that(collectedResultsIndex).isLessThan(100) } } From 53f3795f34c2f0771fb956e744aab6a7a1c627b8 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 6 Dec 2023 10:22:20 -0500 Subject: [PATCH 113/573] QuerySubscriptionIntegrationTest.kt: check for 1000 rather than 100 calls in reload_concurrent_invocations_get_conflated --- .../firebase/dataconnect/QuerySubscriptionIntegrationTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt index a4152274f6f..42b69ab3adc 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt @@ -190,9 +190,9 @@ class QuerySubscriptionIntegrationTest { yield() } - // Verify that the calls to reload() were conflated, by ensuring that we got WAY less than + // Verify that the calls to reload() were conflated, by ensuring that we got less than // 25,000 results. - assertWithMessage("collectedResultsIndex").that(collectedResultsIndex).isLessThan(100) + assertWithMessage("collectedResultsIndex").that(collectedResultsIndex).isLessThan(10000) } } From 10d8912735d5b237a6302e560920de9c249462d9 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 6 Dec 2023 11:45:05 -0500 Subject: [PATCH 114/573] Some performance optimizations to elide mutex lock acquisitions --- .../dataconnect/QueryRefIntegrationTest.kt | 2 +- .../dataconnect/FirebaseDataConnect.kt | 66 +++++++++++-------- .../com/google/firebase/dataconnect/Util.kt | 34 +++++----- 3 files changed, 57 insertions(+), 45 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt index 119acd70f0b..c5b96a97ae2 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -322,7 +322,7 @@ class QueryRefIntegrationTest { val queryRef = personSchema.getPerson val deferreds = buildList { - repeat(25_000) { + repeat(100_000) { // Use `Dispatchers.Default` as the dispatcher for the launched coroutines so that there // will be at least 2 threads used to run the coroutines (as documented by // `Dispatchers.Default`), introducing a guaranteed minimum level of parallelism, ensuring diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index cde5eb3afe8..8222fb4c7a9 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -68,13 +68,15 @@ internal constructor( // All accesses to this variable _must_ have locked `mutex`. private var closed = false - // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference - // to the lazily-created object is obtained, then the mutex can be unlocked and the instance can - // be used. - private val grpcClient = - lazy(LazyThreadSafetyMode.NONE) { - if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") - DataConnectGrpcClient( + // If `grpcClientInitialized` is true then `grpcClient` is guaranteed to be initialized. + // Otherwise, `initializeGrpcClient()` can be called to initialize it. + @Volatile private var grpcClientInitialized = false + private lateinit var grpcClient: DataConnectGrpcClient + + // initializeGrpcClient() MUST be called by a coroutine that has locked `mutex`. + private fun initializeGrpcClient(): DataConnectGrpcClient { + if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") + return DataConnectGrpcClient( context = context, projectId = projectId, serviceId = serviceConfig.serviceId, @@ -87,24 +89,31 @@ internal constructor( executor = blockingExecutor, creatorLoggerId = logger.id, ) - } + .also { + grpcClient = it + grpcClientInitialized = true + } + } + + // If `queryManagerInitialized` is true then `queryManager` is guaranteed to be initialized. + // Otherwise, `initializeQueryManager()` can be called to initialize it. + @Volatile private var queryManagerInitialized = false + private lateinit var queryManager: QueryManager + + // initializeQueryManager() MUST be called by a coroutine that has locked `mutex`. + private suspend fun initializeQueryManager(): QueryManager { + if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") - // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference - // to the lazily-created object is obtained, then the mutex can be unlocked and the instance can - // be used. - private val grpcClientOrNull - get() = if (grpcClient.isInitialized()) grpcClient.value else null - - // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference - // to the lazily-created object is obtained, then the mutex can be unlocked and the instance can - // be used. - private val queryManager by - lazy(LazyThreadSafetyMode.NONE) { - if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") - QueryManager(grpcClient.value, coroutineScope, blockingDispatcher, logger.id) + val grpcClient = if (grpcClientInitialized) grpcClient else initializeGrpcClient() + + return QueryManager(grpcClient, coroutineScope, blockingDispatcher, logger.id).also { + queryManager = it + queryManagerInitialized = true } + } - internal suspend fun getQueryManager(): QueryManager = mutex.withLock { queryManager } + internal suspend fun getQueryManager(): QueryManager = + if (queryManagerInitialized) queryManager else mutex.withLock { initializeQueryManager() } internal suspend fun executeMutation(ref: MutationRef, variables: V) = executeMutation(ref, variables, requestId = Random.nextAlphanumericString()) @@ -113,9 +122,11 @@ internal constructor( ref: MutationRef, variables: V, requestId: String - ) = - mutex - .withLock { grpcClient.value } + ): DataConnectResult { + val grpcClient = + if (grpcClientInitialized) grpcClient else mutex.withLock { initializeGrpcClient() } + + return grpcClient .executeMutation( requestId = requestId, sequenceNumber = nextSequenceNumber(), @@ -133,6 +144,7 @@ internal constructor( } .getOrThrow() .toDataConnectResult(variables) + } private val closeResult = MutableStateFlow?>(null) @@ -174,7 +186,9 @@ internal constructor( kotlin .runCatching { logger.debug { "Closing started" } - mutex.withLock { grpcClientOrNull }?.close() + if (grpcClientInitialized) { + grpcClient.close() + } coroutineScope.cancel() logger.debug { "Closing completed" } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 9664ccffff6..f7bf9220e03 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -123,33 +123,31 @@ internal fun ByteArray.toAlphaNumericString(): String = buildString { * * @param initializer the block to invoke at most once to initialize this object's value. */ -internal class SuspendingLazy(initializer: suspend () -> T) { +internal class SuspendingLazy(initializer: suspend () -> T) { private val mutex = Mutex() - private var initializer: (suspend () -> T)? = initializer - @Volatile private var value: Any? = UninitializedValue + @Volatile private var initializer: (suspend () -> T)? = initializer + private lateinit var value: T - val isInitialized: Boolean = value !== UninitializedValue + val isInitialized: Boolean + get() = initializer === null - suspend fun initialize() { - if (value === UninitializedValue) { - mutex.withLock { - if (value === UninitializedValue) { - value = initializer!!() + suspend fun getValue(): T { + if (initializer === null) { + return value + } + + return mutex.withLock { + if (initializer === null) { + value + } else { + initializer!!().also { + value = it initializer = null } } } } - suspend fun getValue(): T { - initialize() - - @Suppress("UNCHECKED_CAST") return value as T - } - override fun toString(): String = if (isInitialized) value.toString() else "SuspendingLazy value not initialized yet." - - // A sentinel value to use to indicate that the value is not yet initialized. - private companion object UninitializedValue } From a06cad69a065cc3069b146a24af4eb69c2e266ac Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 6 Dec 2023 11:48:22 -0500 Subject: [PATCH 115/573] QueryRefIntegrationTest.kt: revert accidental concurrency increase from 25,000 to 100,000 --- .../com/google/firebase/dataconnect/QueryRefIntegrationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt index c5b96a97ae2..119acd70f0b 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QueryRefIntegrationTest.kt @@ -322,7 +322,7 @@ class QueryRefIntegrationTest { val queryRef = personSchema.getPerson val deferreds = buildList { - repeat(100_000) { + repeat(25_000) { // Use `Dispatchers.Default` as the dispatcher for the launched coroutines so that there // will be at least 2 threads used to run the coroutines (as documented by // `Dispatchers.Default`), introducing a guaranteed minimum level of parallelism, ensuring From ffc6b5336183c6997b90014be1fed7ebed3b8c75 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 6 Dec 2023 12:51:27 -0500 Subject: [PATCH 116/573] FirebaseDataConnect.kt: Use SuspendingLazy to manage the grpcClient and queryManager --- .../dataconnect/FirebaseDataConnect.kt | 54 +++++-------------- .../google/firebase/dataconnect/QueryRef.kt | 5 +- .../firebase/dataconnect/QuerySubscription.kt | 13 ++++- .../com/google/firebase/dataconnect/Util.kt | 37 +++++++------ 4 files changed, 46 insertions(+), 63 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 8222fb4c7a9..d1be8a66cd9 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -68,15 +68,10 @@ internal constructor( // All accesses to this variable _must_ have locked `mutex`. private var closed = false - // If `grpcClientInitialized` is true then `grpcClient` is guaranteed to be initialized. - // Otherwise, `initializeGrpcClient()` can be called to initialize it. - @Volatile private var grpcClientInitialized = false - private lateinit var grpcClient: DataConnectGrpcClient - - // initializeGrpcClient() MUST be called by a coroutine that has locked `mutex`. - private fun initializeGrpcClient(): DataConnectGrpcClient { - if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") - return DataConnectGrpcClient( + private val lazyGrpcClient = + SuspendingLazy(mutex) { + if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") + DataConnectGrpcClient( context = context, projectId = projectId, serviceId = serviceConfig.serviceId, @@ -89,31 +84,14 @@ internal constructor( executor = blockingExecutor, creatorLoggerId = logger.id, ) - .also { - grpcClient = it - grpcClientInitialized = true - } - } - - // If `queryManagerInitialized` is true then `queryManager` is guaranteed to be initialized. - // Otherwise, `initializeQueryManager()` can be called to initialize it. - @Volatile private var queryManagerInitialized = false - private lateinit var queryManager: QueryManager - - // initializeQueryManager() MUST be called by a coroutine that has locked `mutex`. - private suspend fun initializeQueryManager(): QueryManager { - if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") - - val grpcClient = if (grpcClientInitialized) grpcClient else initializeGrpcClient() - - return QueryManager(grpcClient, coroutineScope, blockingDispatcher, logger.id).also { - queryManager = it - queryManagerInitialized = true } - } - internal suspend fun getQueryManager(): QueryManager = - if (queryManagerInitialized) queryManager else mutex.withLock { initializeQueryManager() } + internal val lazyQueryManager = + SuspendingLazy(mutex) { + if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") + val grpcClient = lazyGrpcClient.initializedValueOrNull ?: lazyGrpcClient.getValueLocked() + QueryManager(grpcClient, coroutineScope, blockingDispatcher, logger.id) + } internal suspend fun executeMutation(ref: MutationRef, variables: V) = executeMutation(ref, variables, requestId = Random.nextAlphanumericString()) @@ -122,11 +100,8 @@ internal constructor( ref: MutationRef, variables: V, requestId: String - ): DataConnectResult { - val grpcClient = - if (grpcClientInitialized) grpcClient else mutex.withLock { initializeGrpcClient() } - - return grpcClient + ) = + (lazyGrpcClient.initializedValueOrNull ?: lazyGrpcClient.getValue()) .executeMutation( requestId = requestId, sequenceNumber = nextSequenceNumber(), @@ -144,7 +119,6 @@ internal constructor( } .getOrThrow() .toDataConnectResult(variables) - } private val closeResult = MutableStateFlow?>(null) @@ -186,9 +160,7 @@ internal constructor( kotlin .runCatching { logger.debug { "Closing started" } - if (grpcClientInitialized) { - grpcClient.close() - } + lazyGrpcClient.initializedValueOrNull?.apply { close() } coroutineScope.cancel() logger.debug { "Closing completed" } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt index 352e8900854..c5732dda52b 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt @@ -32,7 +32,10 @@ internal constructor( override suspend fun execute( variables: VariablesType ): DataConnectResult = - dataConnect.getQueryManager().execute(this, variables) + dataConnect.lazyQueryManager.let { lazyQueryManager -> + val queryManager = lazyQueryManager.initializedValueOrNull ?: lazyQueryManager.getValue() + queryManager.execute(this, variables) + } fun subscribe(variables: VariablesType): QuerySubscription = QuerySubscription(this, variables) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt index 0d8c4747393..cca6282bc4f 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt @@ -46,7 +46,12 @@ internal constructor( } collectJob = launch { - query.dataConnect.getQueryManager().onResult( + val queryManager = + query.dataConnect.lazyQueryManager.let { lazyQueryManager -> + lazyQueryManager.initializedValueOrNull ?: lazyQueryManager.getValue() + } + + queryManager.onResult( query, variables, sinceSequenceNumber = cachedResult?.sequenceNumber, @@ -60,7 +65,11 @@ internal constructor( } suspend fun reload() { - query.dataConnect.getQueryManager().execute(query, variables) + val queryManager = + query.dataConnect.lazyQueryManager.let { lazyQueryManager -> + lazyQueryManager.initializedValueOrNull ?: lazyQueryManager.getValue() + } + queryManager.execute(query, variables) } suspend fun update(variables: VariablesType) { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index f7bf9220e03..9c38a9d432a 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -121,32 +121,31 @@ internal fun ByteArray.toAlphaNumericString(): String = buildString { * [LazyThreadSafetyMode.SYNCHRONIZED] with a suspending function and a [Mutex] rather than a * blocking synchronization call. * + * @param mutex the mutex to have locked when `initializer` is invoked; if null (the default) then a + * new lock will be used. * @param initializer the block to invoke at most once to initialize this object's value. */ -internal class SuspendingLazy(initializer: suspend () -> T) { - private val mutex = Mutex() - @Volatile private var initializer: (suspend () -> T)? = initializer - private lateinit var value: T +internal class SuspendingLazy(mutex: Mutex? = null, initializer: suspend () -> T) { + private val mutex = mutex ?: Mutex() + private var initializer: (suspend () -> T)? = initializer + @Volatile private var value: T? = null + + val initializedValueOrNull: T? + get() = value val isInitialized: Boolean - get() = initializer === null + get() = value !== null - suspend fun getValue(): T { - if (initializer === null) { - return value - } + suspend fun getValue(): T = value ?: mutex.withLock { getValueLocked() } - return mutex.withLock { - if (initializer === null) { - value - } else { - initializer!!().also { - value = it - initializer = null - } + // This function _must_ be called by a coroutine that has locked the mutex given to the + // constructor; otherwise, a data race will occur, resulting in undefined behavior. + suspend fun getValueLocked(): T = + value + ?: initializer!!().also { + value = it + initializer = null } - } - } override fun toString(): String = if (isInitialized) value.toString() else "SuspendingLazy value not initialized yet." From 6352211cc9211d732e22feff665e26995f45cdd0 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 6 Dec 2023 13:03:44 -0500 Subject: [PATCH 117/573] FirebaseDataConnect.kt: minor code tweak, no functionality changed --- .../com/google/firebase/dataconnect/FirebaseDataConnect.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index d1be8a66cd9..2d558ad13f8 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -229,7 +229,7 @@ internal constructor( private fun MutableStateFlow?>.clearResultUnlessSuccess() { while (true) { val oldValue = value - if (oldValue != null && oldValue.isSuccess) { + if (oldValue?.isSuccess == true) { return } if (compareAndSet(oldValue, null)) { From 3a1a63f3f03ed394b0d14ffaef861f9873b83d21 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 6 Dec 2023 13:21:29 -0500 Subject: [PATCH 118/573] DataConnectUntypedData.kt: make constructor internal --- .../com/google/firebase/dataconnect/DataConnectUntypedData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt index 65ccc49773d..93b2341a2a0 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt @@ -18,7 +18,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder class DataConnectUntypedData -constructor(val data: Map?, val errors: List) { +internal constructor(val data: Map?, val errors: List) { override fun equals(other: Any?) = (other as? DataConnectUntypedData)?.let { it.data == data && it.errors == errors } ?: false From 8ea4c554a0a6db8ba3c74963a8ee5d45f88ac635 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 6 Dec 2023 13:22:56 -0500 Subject: [PATCH 119/573] DataConnectUntypedData.kt: improve hashCode() implementation --- .../com/google/firebase/dataconnect/DataConnectUntypedData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt index 93b2341a2a0..96ada3e02b2 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt @@ -22,7 +22,7 @@ internal constructor(val data: Map?, val errors: List { From 821fa329a108c5db5b675f7889ddcbb565e21532 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 6 Dec 2023 16:22:29 -0500 Subject: [PATCH 120/573] QueryManager: fix so that all resultFlows emit the exact same sequence of events --- .../firebase/dataconnect/QueryManager.kt | 67 ++++++++++++++----- .../com/google/firebase/dataconnect/Util.kt | 7 ++ 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 767acd29965..a3a63850a94 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -196,8 +196,30 @@ private class RegisteredDataDeserialzer( private val deserializationDispatcher: CoroutineDispatcher, private val logger: Logger ) { - private val latestUpdate = MutableStateFlow?>(null) - private val latestSuccessfulUpdate = MutableStateFlow?>(null) + // A flow that emits a value every time that there is an update, either a successful or an + // unsuccessful update. There is no replay cache in this shared flow because there is no way to + // atomically emit a new event and ensure that it has a larger sequence number, and we don't want + // to "replay" an older result. Use `latestUpdate` instead of relying on the replay cache. + private val updates = + MutableSharedFlow>( + replay = 0, + extraBufferCapacity = Int.MAX_VALUE, + onBufferOverflow = BufferOverflow.SUSPEND, + ) + + // The latest update (i.e. the update with the highest sequence number) that has ever been emitted + // to `updates`. The `ref` of the value will be null if, and only if, no updates have ever + // occurred. + private val latestUpdate = MutableStateFlow>>(NullableReference(null)) + + // The same as `latestUpdate`, except that it only store the latest _successful_ update. That is, + // if there was a successful update followed by a failed update then the value of this flow would + // be that successful update, whereas `latestUpdate` would store the failed one. + // + // This flow is updated by initializing the lazy value from `latestUpdate`; therefore, make sure + // to initialize the lazy value from `latestUpdate` before getting this flow's value. + private val latestSuccessfulUpdate = + MutableStateFlow>>(NullableReference(null)) data class Update( val sequenceNumber: Long, @@ -205,27 +227,33 @@ private class RegisteredDataDeserialzer( ) fun update(requestId: String, sequenceNumber: Long, result: Result) { - // Use a compare-and-swap ("CAS") loop to ensure that an old update never clobbers a newer one. val newUpdate = Update(sequenceNumber = sequenceNumber, result = lazyDeserialize(requestId, result)) + + // Use a compare-and-swap ("CAS") loop to ensure that an old update never clobbers a newer one. while (true) { val currentUpdate = latestUpdate.value - if (currentUpdate !== null && currentUpdate.sequenceNumber > sequenceNumber) { - return // don't clobber a newer update with an older one + if (currentUpdate.ref !== null && currentUpdate.ref.sequenceNumber > sequenceNumber) { + break // don't clobber a newer update with an older one } - if (latestUpdate.compareAndSet(currentUpdate, newUpdate)) { - return + if (latestUpdate.compareAndSet(currentUpdate, NullableReference(newUpdate))) { + break } } + + // Emit to the `updates` shared flow _after_ setting `latestUpdate` to avoid others missing + // the latest update. + val emitSucceeded = updates.tryEmit(newUpdate) + check(emitSucceeded) { "updates.tryEmit(newUpdate) should have returned true" } } suspend fun getLatestUpdate(): Result>? = - latestUpdate.value?.result?.getValue() + latestUpdate.value.ref?.result?.getValue() suspend fun getLatestSuccessfulUpdate(): DeserialzedOperationResult? { // Call getLatestUpdate() to populate `latestSuccessfulUpdate` with the most recent update. getLatestUpdate() - return latestSuccessfulUpdate.value + return latestSuccessfulUpdate.value.ref } suspend fun onSuccessfulUpdate( @@ -233,14 +261,16 @@ private class RegisteredDataDeserialzer( callback: suspend (DeserialzedOperationResult) -> Unit ): Nothing { var lastSequenceNumber = sinceSequenceNumber ?: Long.MIN_VALUE - latestUpdate.collect { update -> - if (update !== null && lastSequenceNumber < update.sequenceNumber) { - update.result.getValue().onSuccess { deserializedOperationResult -> - lastSequenceNumber = deserializedOperationResult.sequenceNumber - callback(deserializedOperationResult) + updates + .onSubscription { latestUpdate.value.ref?.let { emit(it) } } + .collect { update -> + if (update.sequenceNumber > lastSequenceNumber) { + update.result.getValue().onSuccess { deserializedOperationResult -> + lastSequenceNumber = deserializedOperationResult.sequenceNumber + callback(deserializedOperationResult) + } } } - } } private fun lazyDeserialize( @@ -261,10 +291,13 @@ private class RegisteredDataDeserialzer( // that an older result does not clobber a newer one. while (true) { val latestSuccessful = latestSuccessfulUpdate.value - if (latestSuccessful !== null && latestSuccessful.sequenceNumber >= it.sequenceNumber) { + if ( + latestSuccessful.ref !== null && + it.sequenceNumber <= latestSuccessful.ref.sequenceNumber + ) { break } - if (latestSuccessfulUpdate.compareAndSet(latestSuccessful, it)) { + if (latestSuccessfulUpdate.compareAndSet(latestSuccessful, NullableReference(it))) { break } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 9c38a9d432a..1752fb9e263 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -150,3 +150,10 @@ internal class SuspendingLazy(mutex: Mutex? = null, initializer: suspen override fun toString(): String = if (isInitialized) value.toString() else "SuspendingLazy value not initialized yet." } + +internal class NullableReference(val ref: T?) { + override fun equals(other: Any?) = + (other as? NullableReference<*>)?.let { ref == it.ref } ?: false + override fun hashCode() = ref?.hashCode() ?: 0 + override fun toString() = ref?.toString() ?: "null" +} From e3e5196d3901bf8eb63b46f978dea79acf2ae981 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 7 Dec 2023 00:11:02 -0500 Subject: [PATCH 121/573] random fixes and improvements --- .../QuerySubscriptionIntegrationTest.kt | 73 ++++++++++--------- .../dataconnect/FirebaseDataConnect.kt | 13 +++- .../firebase/dataconnect/QueryManager.kt | 51 +++++++++---- 3 files changed, 83 insertions(+), 54 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt index 42b69ab3adc..f3721ce9abe 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt @@ -153,47 +153,50 @@ class QuerySubscriptionIntegrationTest { } @Test - fun reload_concurrent_invocations_get_conflated() = runTest { - schema.createPerson.execute(id = "TestId12345", name = "Name", age = 10000) - val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - - val resultCollected = MutableStateFlow(false) - val collectedResults = - CopyOnWriteArrayList>() - backgroundScope.launch { - querySubscription.resultFlow.onEach { resultCollected.value = true }.toList(collectedResults) - } + fun reload_concurrent_invocations_get_conflated() = + runTest(timeout = 60.seconds) { + schema.createPerson.execute(id = "TestId12345", name = "Name", age = 10000) + val querySubscription = schema.getPerson.subscribe(id = "TestId12345") + + val resultCollected = MutableStateFlow(false) + val collectedResults = + CopyOnWriteArrayList>() + backgroundScope.launch { + querySubscription.resultFlow + .onEach { resultCollected.value = true } + .toList(collectedResults) + } - val deferreds = buildList { - repeat(25_000) { - // Use `Dispatchers.Default` as the dispatcher for the launched coroutines so that there - // will be at least 2 threads used to run the coroutines (as documented by - // `Dispatchers.Default`), introducing a guaranteed minimum level of parallelism, ensuring - // that this test is indeed testing "massive concurrency". - add(backgroundScope.async(Dispatchers.Default) { querySubscription.reload() }) + val deferreds = buildList { + repeat(25_000) { + // Use `Dispatchers.Default` as the dispatcher for the launched coroutines so that there + // will be at least 2 threads used to run the coroutines (as documented by + // `Dispatchers.Default`), introducing a guaranteed minimum level of parallelism, ensuring + // that this test is indeed testing "massive concurrency". + add(backgroundScope.async(Dispatchers.Default) { querySubscription.reload() }) + } } - } - // Wait for at least one result to come in. - resultCollected.filter { it }.first() + // Wait for at least one result to come in. + resultCollected.filter { it }.first() - // Wait for all calls to reload() to complete. - deferreds.forEach { it.await() } + // Wait for all calls to reload() to complete. + deferreds.forEach { it.await() } - // Verify that we got the expected results. - var collectedResultsIndex = 0 - while (collectedResultsIndex < collectedResults.size) { - assertWithMessage("collectedResults[$collectedResultsIndex]") - .that(collectedResults[collectedResultsIndex].data.person) - .isEqualToGetPersonQueryResult(name = "Name", age = 10000) - collectedResultsIndex++ - yield() - } + // Verify that we got the expected results. + var collectedResultsIndex = 0 + while (collectedResultsIndex < collectedResults.size) { + assertWithMessage("collectedResults[$collectedResultsIndex]") + .that(collectedResults[collectedResultsIndex].data.person) + .isEqualToGetPersonQueryResult(name = "Name", age = 10000) + collectedResultsIndex++ + yield() + } - // Verify that the calls to reload() were conflated, by ensuring that we got less than - // 25,000 results. - assertWithMessage("collectedResultsIndex").that(collectedResultsIndex).isLessThan(10000) - } + // Verify that the calls to reload() were conflated, by ensuring that we got less than + // 25,000 results. + assertWithMessage("collectedResultsIndex").that(collectedResultsIndex).isLessThan(10000) + } } private fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 2d558ad13f8..773df42efc6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -50,17 +50,18 @@ internal constructor( } } - internal val coroutineScope = + private val coroutineScope = CoroutineScope( SupervisorJob() + nonBlockingExecutor.asCoroutineDispatcher() + CoroutineName("FirebaseDataConnect") + - CoroutineExceptionHandler { coroutineContext, throwable -> + CoroutineExceptionHandler { _, throwable -> logger.warn(throwable) { "uncaught exception from a coroutine" } } ) private val blockingDispatcher = blockingExecutor.asCoroutineDispatcher() + private val nonBlockingDispatcher = nonBlockingExecutor.asCoroutineDispatcher() // Protects `closed`, `grpcClient`, and `queryManager`. private val mutex = Mutex() @@ -90,7 +91,13 @@ internal constructor( SuspendingLazy(mutex) { if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") val grpcClient = lazyGrpcClient.initializedValueOrNull ?: lazyGrpcClient.getValueLocked() - QueryManager(grpcClient, coroutineScope, blockingDispatcher, logger.id) + QueryManager( + grpcClient = grpcClient, + coroutineScope = coroutineScope, + blockingDispatcher = nonBlockingDispatcher, + nonBlockingDispatcher = nonBlockingDispatcher, + creatorLoggerId = logger.id + ) } internal suspend fun executeMutation(ref: MutationRef, variables: V) = diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index a3a63850a94..6497e74a542 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -27,13 +27,20 @@ import kotlinx.serialization.DeserializationStrategy internal class QueryManager( grpcClient: DataConnectGrpcClient, coroutineScope: CoroutineScope, - deserializationDispatcher: CoroutineDispatcher, + blockingDispatcher: CoroutineDispatcher, + nonBlockingDispatcher: CoroutineDispatcher, creatorLoggerId: String ) { private val logger = Logger("QueryManager").apply { debug { "Created from $creatorLoggerId" } } private val liveQueries = - LiveQueries(grpcClient, coroutineScope, deserializationDispatcher, logger) + LiveQueries( + grpcClient = grpcClient, + coroutineScope = coroutineScope, + blockingDispatcher = blockingDispatcher, + nonBlockingDispatcher = nonBlockingDispatcher, + logger = logger + ) suspend fun execute(ref: QueryRef, variables: V): DataConnectResult = liveQueries @@ -63,10 +70,18 @@ private class LiveQuery( private val grpcClient: DataConnectGrpcClient, private val operationName: String, private val variables: Struct, - private val coroutineScope: CoroutineScope, - private val deserializationDispatcher: CoroutineDispatcher, + coroutineScope: CoroutineScope, + private val blockingDispatcher: CoroutineDispatcher, + nonBlockingDispatcher: CoroutineDispatcher, private val logger: Logger, -) { +) : AutoCloseable { + private val coroutineScope = + CoroutineScope( + SupervisorJob(coroutineScope.coroutineContext[Job]) + + nonBlockingDispatcher + + CoroutineName("LiveQuery[$operationName ${variables.toCompactString()}]") + ) + // The `dataDeserializers` list may be safely read concurrently from multiple threads, as it uses // a `CopyOnWriteArrayList` that is completely thread-safe. Any mutating operations must be // performed while the `dataDeserializersWriteMutex` mutex is locked, so that @@ -83,8 +98,7 @@ private class LiveQuery( // Register the data deserialzier _before_ waiting for the current job to complete. This // guarantees that the deserializer will be registered by the time the subsequent job (`newJob` // below) runs. - val registeredDataDeserializer = - registerDataDeserializer(dataDeserializer, deserializationDispatcher) + val registeredDataDeserializer = registerDataDeserializer(dataDeserializer) // Wait for the current job to complete (if any), and ignore its result. Waiting avoids running // multiple queries in parallel, which would not scale. @@ -116,12 +130,11 @@ private class LiveQuery( executeQuery: Boolean, callback: suspend (DeserialzedOperationResult) -> Unit, ): Nothing { - val registeredDataDeserialzer = - registerDataDeserializer(dataDeserializer, deserializationDispatcher) + val registeredDataDeserializer = registerDataDeserializer(dataDeserializer) // Immediately deliver the most recent update to the callback, so the collector has some data // to work with while waiting for the network requests to complete. - val cachedUpdate = registeredDataDeserialzer.getLatestSuccessfulUpdate() + val cachedUpdate = registeredDataDeserializer.getLatestSuccessfulUpdate() val effectiveSinceSequenceNumber = if (cachedUpdate === null) { sinceSequenceNumber @@ -141,7 +154,7 @@ private class LiveQuery( coroutineScope.launch { runCatching { execute(dataDeserializer) } } } - registeredDataDeserialzer.onSuccessfulUpdate( + registeredDataDeserializer.onSuccessfulUpdate( sinceSequenceNumber = effectiveSinceSequenceNumber ) { callback(it) @@ -169,8 +182,7 @@ private class LiveQuery( @Suppress("UNCHECKED_CAST") private suspend fun registerDataDeserializer( - dataDeserializer: DeserializationStrategy, - deserializationDispatcher: CoroutineDispatcher, + dataDeserializer: DeserializationStrategy ): RegisteredDataDeserialzer = // First, check if the deserializer is already registered and, if it is, just return it. // Otherwise, lock the "write" mutex and register it. We still have to check again if it is @@ -183,12 +195,16 @@ private class LiveQuery( dataDeserializers .firstOrNull { it.deserializer === dataDeserializer } ?.let { it as RegisteredDataDeserialzer } - ?: RegisteredDataDeserialzer(dataDeserializer, deserializationDispatcher, logger).also { + ?: RegisteredDataDeserialzer(dataDeserializer, blockingDispatcher, logger).also { dataDeserializers.add(it) } } data class Key(val operationName: String, val variablesHash: String) + + override fun close() { + coroutineScope.cancel() + } } private class RegisteredDataDeserialzer( @@ -308,7 +324,8 @@ private class RegisteredDataDeserialzer( private class LiveQueries( private val grpcClient: DataConnectGrpcClient, private val coroutineScope: CoroutineScope, - private val deserializationDispatcher: CoroutineDispatcher, + private val blockingDispatcher: CoroutineDispatcher, + private val nonBlockingDispatcher: CoroutineDispatcher, private val logger: Logger, ) { private val mutex = Mutex() @@ -353,7 +370,8 @@ private class LiveQueries( operationName = ref.operationName, variables = variablesStruct, coroutineScope = coroutineScope, - deserializationDispatcher = deserializationDispatcher, + blockingDispatcher = blockingDispatcher, + nonBlockingDispatcher = nonBlockingDispatcher, logger = logger, ), refCount = 0 @@ -378,6 +396,7 @@ private class LiveQueries( referenceCountedLiveQuery.refCount-- if (referenceCountedLiveQuery.refCount == 0) { referenceCountedLiveQueryByKey.remove(liveQuery.key) + liveQuery.close() } } } From 7c2a152f14a72b1ee30b5cb304b980ebf1dabf9f Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 7 Dec 2023 00:15:19 -0500 Subject: [PATCH 122/573] QueryManager.kt: fix usage of NonCancellable --- .../main/kotlin/com/google/firebase/dataconnect/QueryManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 6497e74a542..ba13c5c36cb 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -346,7 +346,7 @@ private class LiveQueries( return try { block(liveQuery) } finally { - mutex.withLock { withContext(NonCancellable) { releaseLiveQuery(liveQuery) } } + withContext(NonCancellable) { mutex.withLock { releaseLiveQuery(liveQuery) } } } } From 0cf508d42b12fad3ce94285af33e92ebc0cf12fd Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 7 Dec 2023 13:18:33 -0500 Subject: [PATCH 123/573] some improvements to logging, naming, and another use for SuspendingLazy --- .../dataconnect/DataConnectGrpcClient.kt | 83 +++++++--------- .../dataconnect/FirebaseDataConnect.kt | 6 +- .../com/google/firebase/dataconnect/Logger.kt | 97 +++++++++++-------- .../firebase/dataconnect/QueryManager.kt | 55 ++++++++--- .../com/google/firebase/dataconnect/Util.kt | 18 +++- 5 files changed, 149 insertions(+), 110 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 2be1bbd378f..b98c8f8f5af 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -27,9 +27,7 @@ import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder import java.util.concurrent.Executor import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.completeWith +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.serialization.DeserializationStrategy @@ -44,37 +42,30 @@ internal class DataConnectGrpcClient( hostName: String, port: Int, sslEnabled: Boolean, - private val executor: Executor, - creatorLoggerId: String, + private val blockingExecutor: Executor, + parentLogger: Logger, ) { private val logger = - Logger("DataConnectGrpcClient").apply { debug { "Created from $creatorLoggerId" } } + Logger("DataConnectGrpcClient").apply { debug { "Created by ${parentLogger.nameWithId}" } } private val requestName = "projects/$projectId/locations/$location/services/$serviceId/" + "operationSets/$operationSet/revisions/$revision" - // Protects `closed`, `grpcChannel`, and `grpcStub`. - private val mutex = Mutex() - - // All accesses to this variable _must_ have locked `mutex`. + private val closedMutex = Mutex() private var closed = false - // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference - // to the lazily-created object is obtained, then the mutex can be unlocked and the instance can - // be used. - private val grpcChannel = - lazy(LazyThreadSafetyMode.NONE) { - logger.debug { "${ManagedChannel::class.qualifiedName} initialization started" } - + private val lazyGrpcChannel = + SuspendingLazy(closedMutex, blockingExecutor.asCoroutineDispatcher()) { if (closed) throw IllegalStateException("DataConnectGrpcClient instance has been closed") + logger.debug { "${ManagedChannel::class.qualifiedName} initialization started" } + // Upgrade the Android security provider using Google Play Services. // // We need to upgrade the Security Provider before any network channels are initialized - // because - // okhttp maintains a list of supported providers that is initialized when the JVM first - // resolves the static dependencies of ManagedChannel. + // because okhttp maintains a list of supported providers that is initialized when the JVM + // first resolves the static dependencies of ManagedChannel. // // If initialization fails for any reason, then a warning is logged and the original, // un-upgraded security provider is used. @@ -95,7 +86,7 @@ internal class DataConnectGrpcClient( // failsafe. it.keepAliveTime(30, TimeUnit.SECONDS) - it.executor(executor) + it.executor(blockingExecutor) // Wrap the `ManagedChannelBuilder` in an `AndroidChannelBuilder`. This allows the channel // to respond more gracefully to network change events, such as switching from cellular to @@ -108,14 +99,7 @@ internal class DataConnectGrpcClient( channel } - private val grpcChannelOrNull - get() = if (grpcChannel.isInitialized()) grpcChannel.value else null - - // All accesses to this variable _must_ have locked `mutex`. Note, however, that once a reference - // to the lazily-created object is obtained, then the mutex can be unlocked and the instance can - // be used. - private val grpcStub: DataServiceCoroutineStub by - lazy(LazyThreadSafetyMode.NONE) { DataServiceCoroutineStub(grpcChannel.value) } + private val lazyGrpcStub = SuspendingLazy { DataServiceCoroutineStub(lazyGrpcChannel.getValue()) } data class OperationResult( val data: Struct?, @@ -142,8 +126,8 @@ internal class DataConnectGrpcClient( "ExecuteQueryRequest: ${request.toCompactString()}" } val response = - mutex - .withLock { grpcStub } + lazyGrpcStub + .getValue() .runCatching { executeQuery(request) } .onFailure { logger.warn(it) { @@ -180,8 +164,8 @@ internal class DataConnectGrpcClient( "ExecuteMutationRequest: ${request.toCompactString()}" } val response = - mutex - .withLock { grpcStub } + lazyGrpcStub + .getValue() .runCatching { executeMutation(request) } .onFailure { logger.warn(it) { @@ -202,34 +186,35 @@ internal class DataConnectGrpcClient( } private val closingMutex = Mutex() + private var awaitTerminationJob: Deferred? = null private var closeCompleted = false suspend fun close() { - val grpcChannel = - mutex.withLock { - closed = true - grpcChannelOrNull - } + closedMutex.withLock { closed = true } closingMutex.withLock { if (!closeCompleted) { - grpcChannel?.terminate() + closeGrpcChannel() } closeCompleted = true } } - private suspend fun ManagedChannel.terminate() { - shutdown() - - val deferred = CompletableDeferred() - thread(isDaemon = true, name = "ManagedChannel.terminate() from ${logger.id}") { - deferred.completeWith( - runCatching { awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS) } - ) - } + // This function _must_ be called by a coroutine that has acquired the lock on `closingMutex`. + @OptIn(DelicateCoroutinesApi::class) + private suspend fun closeGrpcChannel() { + val grpcChannel = lazyGrpcChannel.initializedValueOrNull ?: return + + val job = + awaitTerminationJob?.let { if (it.isCancelled && it.isCompleted) null else it } + ?: GlobalScope.async { + withContext(blockingExecutor.asCoroutineDispatcher()) { + grpcChannel.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS) + } + } + .also { awaitTerminationJob = it } - deferred.await() + job.await() } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 773df42efc6..e95f7b3367a 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -82,8 +82,8 @@ internal constructor( hostName = settings.hostName, port = settings.port, sslEnabled = settings.sslEnabled, - executor = blockingExecutor, - creatorLoggerId = logger.id, + blockingExecutor = blockingExecutor, + parentLogger = logger, ) } @@ -96,7 +96,7 @@ internal constructor( coroutineScope = coroutineScope, blockingDispatcher = nonBlockingDispatcher, nonBlockingDispatcher = nonBlockingDispatcher, - creatorLoggerId = logger.id + parentLogger = logger, ) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt index 34be6415d63..49d0677f7c4 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Logger.kt @@ -20,64 +20,77 @@ enum class LogLevel { DEBUG, INFO, WARNING, + ERROR, } @Volatile var logLevel: LogLevel = LogLevel.INFO internal interface Logger { - val id: String + val name: String + val idz: String + val nameWithId: String - fun info(message: () -> Any?) - fun debug(message: () -> Any?) - fun warn(message: () -> Any?) - fun warn(e: Throwable?, message: () -> Any?) + fun log(exception: Throwable?, level: LogLevel, message: String) } -internal fun Logger(name: String): Logger = LoggerImpl(name) +internal inline fun Logger.debug(message: () -> Any?) { + if (logLevel <= LogLevel.DEBUG) debug("${message()}") +} -private const val LOG_TAG = "FirebaseDataConnect" +internal fun Logger.debug(message: String) { + if (logLevel <= LogLevel.DEBUG) log(null, LogLevel.DEBUG, message) +} -private fun isLogEnabledFor(level: LogLevel) = - when (logLevel) { - LogLevel.DEBUG -> - when (level) { - LogLevel.DEBUG -> true - LogLevel.INFO -> true - LogLevel.WARNING -> true - } - LogLevel.INFO -> - when (level) { - LogLevel.DEBUG -> false - LogLevel.INFO -> true - LogLevel.WARNING -> true - } - LogLevel.WARNING -> - when (level) { - LogLevel.DEBUG -> false - LogLevel.INFO -> false - LogLevel.WARNING -> true - } - } +internal inline fun Logger.info(message: () -> Any?) { + if (logLevel <= LogLevel.INFO) info("${message()}") +} -private fun runIfLogEnabled(level: LogLevel, block: () -> Unit) { - if (isLogEnabledFor(level)) { - block() - } +internal fun Logger.info(message: String) { + if (logLevel <= LogLevel.INFO) log(null, LogLevel.INFO, message) +} + +internal inline fun Logger.warn(message: () -> Any?) { + if (logLevel <= LogLevel.WARNING) warn("${message()}") +} + +internal inline fun Logger.warn(exception: Throwable?, message: () -> Any?) { + if (logLevel <= LogLevel.WARNING) warn(exception, "${message()}") } -private class LoggerImpl(private val name: String) : Logger { +internal fun Logger.warn(message: String) { + warn(null, message) +} - override val id: String by - lazy(LazyThreadSafetyMode.PUBLICATION) { "$name[id=${Random.nextAlphanumericString()}]" } +internal fun Logger.warn(exception: Throwable?, message: String) { + if (logLevel <= LogLevel.WARNING) log(exception, LogLevel.WARNING, message) +} - override fun info(message: () -> Any?) = - runIfLogEnabled(LogLevel.INFO) { Log.i(LOG_TAG, "$id ${message()}") } +internal fun Logger.error(message: String) { + error(null, message) +} - override fun debug(message: () -> Any?) = - runIfLogEnabled(LogLevel.DEBUG) { Log.d(LOG_TAG, "$id ${message()}") } +internal fun Logger.error(exception: Throwable?, message: String) { + log(exception, LogLevel.ERROR, message) +} - override fun warn(message: () -> Any?) = warn(null, message) +internal fun Logger(name: String): Logger = LoggerImpl(name) - override fun warn(e: Throwable?, message: () -> Any?) = - runIfLogEnabled(LogLevel.WARNING) { Log.w(LOG_TAG, "$id ${message()}", e) } +private const val LOG_TAG = "FirebaseDataConnect" + +private class LoggerImpl(override val name: String) : Logger { + + override val idz: String by + lazy(LazyThreadSafetyMode.PUBLICATION) { Random.nextAlphanumericString() } + + override val nameWithId: String by lazy(LazyThreadSafetyMode.PUBLICATION) { "$name[id=$idz]" } + + override fun log(exception: Throwable?, level: LogLevel, message: String) { + val fullMessage = "$nameWithId $message" + when (level) { + LogLevel.DEBUG -> Log.d(LOG_TAG, fullMessage, exception) + LogLevel.INFO -> Log.i(LOG_TAG, fullMessage, exception) + LogLevel.WARNING -> Log.w(LOG_TAG, fullMessage, exception) + LogLevel.ERROR -> Log.e(LOG_TAG, fullMessage, exception) + } + } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index ba13c5c36cb..3a23abc2f31 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -29,9 +29,10 @@ internal class QueryManager( coroutineScope: CoroutineScope, blockingDispatcher: CoroutineDispatcher, nonBlockingDispatcher: CoroutineDispatcher, - creatorLoggerId: String + parentLogger: Logger, ) { - private val logger = Logger("QueryManager").apply { debug { "Created from $creatorLoggerId" } } + private val logger = + Logger("QueryManager").apply { debug { "Created by ${parentLogger.nameWithId}" } } private val liveQueries = LiveQueries( @@ -39,7 +40,7 @@ internal class QueryManager( coroutineScope = coroutineScope, blockingDispatcher = blockingDispatcher, nonBlockingDispatcher = nonBlockingDispatcher, - logger = logger + parentLogger = logger ) suspend fun execute(ref: QueryRef, variables: V): DataConnectResult = @@ -73,8 +74,13 @@ private class LiveQuery( coroutineScope: CoroutineScope, private val blockingDispatcher: CoroutineDispatcher, nonBlockingDispatcher: CoroutineDispatcher, - private val logger: Logger, + parentLogger: Logger, ) : AutoCloseable { + private val logger = + Logger("LiveQuery").apply { + debug { "Created by ${parentLogger.nameWithId} " + "with key: $key" } + } + private val coroutineScope = CoroutineScope( SupervisorJob(coroutineScope.coroutineContext[Job]) + @@ -167,6 +173,7 @@ private class LiveQuery( val executeQueryResult = grpcClient.runCatching { + logger.debug("Calling executeQuery() with requestId=$requestId") executeQuery( requestId = requestId, operationName = operationName, @@ -189,29 +196,45 @@ private class LiveQuery( // already registered because another thread could have concurrently registered it since we last // checked above. dataDeserializers - .firstOrNull { it.deserializer === dataDeserializer } + .firstOrNull { it.dataDeserializer === dataDeserializer } ?.let { it as RegisteredDataDeserialzer } ?: dataDeserializersWriteMutex.withLock { dataDeserializers - .firstOrNull { it.deserializer === dataDeserializer } + .firstOrNull { it.dataDeserializer === dataDeserializer } ?.let { it as RegisteredDataDeserialzer } - ?: RegisteredDataDeserialzer(dataDeserializer, blockingDispatcher, logger).also { - dataDeserializers.add(it) + ?: Random.nextAlphanumericString().let { registrationId -> + logger.debug { + "Registering data deserializer $dataDeserializer with registrationId=$registrationId" + } + RegisteredDataDeserialzer( + registrationId = registrationId, + dataDeserializer = dataDeserializer, + blockingDispatcher = blockingDispatcher, + parentLogger = logger + ) + .also { dataDeserializers.add(it) } } } data class Key(val operationName: String, val variablesHash: String) override fun close() { + logger.debug("close() called") coroutineScope.cancel() } } private class RegisteredDataDeserialzer( - val deserializer: DeserializationStrategy, - private val deserializationDispatcher: CoroutineDispatcher, - private val logger: Logger + registrationId: String, + val dataDeserializer: DeserializationStrategy, + private val blockingDispatcher: CoroutineDispatcher, + parentLogger: Logger ) { + private val logger = + Logger("RegisteredDataDeserialzer").apply { + debug { "Created by ${parentLogger.nameWithId} " + "with registrationId=$registrationId" } + } + // A flow that emits a value every time that there is an update, either a successful or an // unsuccessful update. There is no replay cache in this shared flow because there is no way to // atomically emit a new event and ensure that it has a larger sequence number, and we don't want @@ -294,7 +317,7 @@ private class RegisteredDataDeserialzer( result: Result ): SuspendingLazy>> = SuspendingLazy { result - .mapCatching { withContext(deserializationDispatcher) { it.deserialize(deserializer) } } + .mapCatching { withContext(blockingDispatcher) { it.deserialize(dataDeserializer) } } .onFailure { // If the overall result was successful then the failure _must_ have occurred during // deserialization. Log the deserialization failure so it doesn't go unnoticed. @@ -326,8 +349,11 @@ private class LiveQueries( private val coroutineScope: CoroutineScope, private val blockingDispatcher: CoroutineDispatcher, private val nonBlockingDispatcher: CoroutineDispatcher, - private val logger: Logger, + parentLogger: Logger, ) { + private val logger = + Logger("LiveQueries").apply { debug { "Created by ${parentLogger.nameWithId}" } } + private val mutex = Mutex() // NOTE: All accesses to `referenceCountedLiveQueryByKey` and the `refCount` field of each value @@ -372,7 +398,7 @@ private class LiveQueries( coroutineScope = coroutineScope, blockingDispatcher = blockingDispatcher, nonBlockingDispatcher = nonBlockingDispatcher, - logger = logger, + parentLogger = logger, ), refCount = 0 ) @@ -395,6 +421,7 @@ private class LiveQueries( referenceCountedLiveQuery.refCount-- if (referenceCountedLiveQuery.refCount == 0) { + logger.debug { "refCount==0 for LiveQuery with key=${liveQuery.key}; removing the mapping" } referenceCountedLiveQueryByKey.remove(liveQuery.key) liveQuery.close() } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 1752fb9e263..85b1e332893 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -15,8 +15,10 @@ package com.google.firebase.dataconnect import java.io.OutputStream import java.util.concurrent.atomic.AtomicLong +import kotlin.coroutines.CoroutineContext import kotlin.math.abs import kotlin.random.Random +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -123,9 +125,16 @@ internal fun ByteArray.toAlphaNumericString(): String = buildString { * * @param mutex the mutex to have locked when `initializer` is invoked; if null (the default) then a * new lock will be used. + * @param coroutineContext the coroutine context with which to invoke `initializer`; if null (the + * default) then the context of the coroutine that calls [getValue] or [getValueLocked] will be + * used. * @param initializer the block to invoke at most once to initialize this object's value. */ -internal class SuspendingLazy(mutex: Mutex? = null, initializer: suspend () -> T) { +internal class SuspendingLazy( + mutex: Mutex? = null, + private val coroutineContext: CoroutineContext? = null, + initializer: suspend () -> T +) { private val mutex = mutex ?: Mutex() private var initializer: (suspend () -> T)? = initializer @Volatile private var value: T? = null @@ -136,11 +145,16 @@ internal class SuspendingLazy(mutex: Mutex? = null, initializer: suspen val isInitialized: Boolean get() = value !== null - suspend fun getValue(): T = value ?: mutex.withLock { getValueLocked() } + suspend inline fun getValue(): T = value ?: mutex.withLock { getValueLocked() } // This function _must_ be called by a coroutine that has locked the mutex given to the // constructor; otherwise, a data race will occur, resulting in undefined behavior. suspend fun getValueLocked(): T = + if (coroutineContext === null) { + getValueLocked0() + } else withContext(coroutineContext) { getValueLocked0() } + + private suspend inline fun getValueLocked0(): T = value ?: initializer!!().also { value = it From 98a4782f910bab6b62e95ab8e81d50b2945c6b4f Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 7 Dec 2023 13:19:25 -0500 Subject: [PATCH 124/573] minor style fix --- .../src/main/kotlin/com/google/firebase/dataconnect/Util.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 85b1e332893..263d2a46624 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -152,7 +152,9 @@ internal class SuspendingLazy( suspend fun getValueLocked(): T = if (coroutineContext === null) { getValueLocked0() - } else withContext(coroutineContext) { getValueLocked0() } + } else { + withContext(coroutineContext) { getValueLocked0() } + } private suspend inline fun getValueLocked0(): T = value From 0c6c821428f22b9aca0665c73d121da5a782ba3f Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 8 Dec 2023 11:03:49 -0500 Subject: [PATCH 125/573] QuerySubscription.kt: tiny code improvement --- .../com/google/firebase/dataconnect/QuerySubscription.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt index cca6282bc4f..85f21045d82 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt @@ -22,10 +22,10 @@ internal constructor( variables: VariablesType ) { private val _variables = MutableStateFlow(variables) - val variables: VariablesType by _variables.asStateFlow()::value + val variables: VariablesType by _variables::value private val _lastResult = MutableStateFlow?>(null) - val lastResult: DataConnectResult? by _lastResult.asStateFlow()::value + val lastResult: DataConnectResult? by _lastResult::value // Each collection of this flow triggers an implicit `reload()`. val resultFlow: Flow> = channelFlow { From de4bb222cda257c755bbe0faa94a6b2757bba2ea Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 8 Dec 2023 11:14:31 -0500 Subject: [PATCH 126/573] random cleanups --- .../dataconnect/DataConnectGrpcClient.kt | 6 +++--- .../dataconnect/FirebaseDataConnect.kt | 8 ++++---- .../firebase/dataconnect/QueryManager.kt | 4 ++-- .../google/firebase/dataconnect/QueryRef.kt | 5 +---- .../firebase/dataconnect/QuerySubscription.kt | 11 ++--------- .../com/google/firebase/dataconnect/Util.kt | 18 +++++++----------- 6 files changed, 19 insertions(+), 33 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index b98c8f8f5af..039260cd0cd 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -99,7 +99,7 @@ internal class DataConnectGrpcClient( channel } - private val lazyGrpcStub = SuspendingLazy { DataServiceCoroutineStub(lazyGrpcChannel.getValue()) } + private val lazyGrpcStub = SuspendingLazy { DataServiceCoroutineStub(lazyGrpcChannel.get()) } data class OperationResult( val data: Struct?, @@ -127,7 +127,7 @@ internal class DataConnectGrpcClient( } val response = lazyGrpcStub - .getValue() + .get() .runCatching { executeQuery(request) } .onFailure { logger.warn(it) { @@ -165,7 +165,7 @@ internal class DataConnectGrpcClient( } val response = lazyGrpcStub - .getValue() + .get() .runCatching { executeMutation(request) } .onFailure { logger.warn(it) { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index e95f7b3367a..93a6369fffa 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -90,11 +90,10 @@ internal constructor( internal val lazyQueryManager = SuspendingLazy(mutex) { if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed") - val grpcClient = lazyGrpcClient.initializedValueOrNull ?: lazyGrpcClient.getValueLocked() QueryManager( - grpcClient = grpcClient, + grpcClient = lazyGrpcClient.getLocked(), coroutineScope = coroutineScope, - blockingDispatcher = nonBlockingDispatcher, + blockingDispatcher = blockingDispatcher, nonBlockingDispatcher = nonBlockingDispatcher, parentLogger = logger, ) @@ -108,7 +107,8 @@ internal constructor( variables: V, requestId: String ) = - (lazyGrpcClient.initializedValueOrNull ?: lazyGrpcClient.getValue()) + lazyGrpcClient + .get() .executeMutation( requestId = requestId, sequenceNumber = nextSequenceNumber(), diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index 3a23abc2f31..c561f44ca34 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -287,7 +287,7 @@ private class RegisteredDataDeserialzer( } suspend fun getLatestUpdate(): Result>? = - latestUpdate.value.ref?.result?.getValue() + latestUpdate.value.ref?.result?.get() suspend fun getLatestSuccessfulUpdate(): DeserialzedOperationResult? { // Call getLatestUpdate() to populate `latestSuccessfulUpdate` with the most recent update. @@ -304,7 +304,7 @@ private class RegisteredDataDeserialzer( .onSubscription { latestUpdate.value.ref?.let { emit(it) } } .collect { update -> if (update.sequenceNumber > lastSequenceNumber) { - update.result.getValue().onSuccess { deserializedOperationResult -> + update.result.get().onSuccess { deserializedOperationResult -> lastSequenceNumber = deserializedOperationResult.sequenceNumber callback(deserializedOperationResult) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt index c5732dda52b..f1b7fa9194c 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryRef.kt @@ -32,10 +32,7 @@ internal constructor( override suspend fun execute( variables: VariablesType ): DataConnectResult = - dataConnect.lazyQueryManager.let { lazyQueryManager -> - val queryManager = lazyQueryManager.initializedValueOrNull ?: lazyQueryManager.getValue() - queryManager.execute(this, variables) - } + dataConnect.lazyQueryManager.get().execute(this, variables) fun subscribe(variables: VariablesType): QuerySubscription = QuerySubscription(this, variables) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt index 85f21045d82..3febf53109f 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt @@ -46,11 +46,7 @@ internal constructor( } collectJob = launch { - val queryManager = - query.dataConnect.lazyQueryManager.let { lazyQueryManager -> - lazyQueryManager.initializedValueOrNull ?: lazyQueryManager.getValue() - } - + val queryManager = query.dataConnect.lazyQueryManager.get() queryManager.onResult( query, variables, @@ -65,10 +61,7 @@ internal constructor( } suspend fun reload() { - val queryManager = - query.dataConnect.lazyQueryManager.let { lazyQueryManager -> - lazyQueryManager.initializedValueOrNull ?: lazyQueryManager.getValue() - } + val queryManager = query.dataConnect.lazyQueryManager.get() queryManager.execute(query, variables) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt index 263d2a46624..5a27bfa309e 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/Util.kt @@ -126,8 +126,7 @@ internal fun ByteArray.toAlphaNumericString(): String = buildString { * @param mutex the mutex to have locked when `initializer` is invoked; if null (the default) then a * new lock will be used. * @param coroutineContext the coroutine context with which to invoke `initializer`; if null (the - * default) then the context of the coroutine that calls [getValue] or [getValueLocked] will be - * used. + * default) then the context of the coroutine that calls [get] or [getLocked] will be used. * @param initializer the block to invoke at most once to initialize this object's value. */ internal class SuspendingLazy( @@ -142,21 +141,18 @@ internal class SuspendingLazy( val initializedValueOrNull: T? get() = value - val isInitialized: Boolean - get() = value !== null - - suspend inline fun getValue(): T = value ?: mutex.withLock { getValueLocked() } + suspend inline fun get(): T = value ?: mutex.withLock { getLocked() } // This function _must_ be called by a coroutine that has locked the mutex given to the // constructor; otherwise, a data race will occur, resulting in undefined behavior. - suspend fun getValueLocked(): T = + suspend fun getLocked(): T = if (coroutineContext === null) { - getValueLocked0() + getLockedInContext() } else { - withContext(coroutineContext) { getValueLocked0() } + withContext(coroutineContext) { getLockedInContext() } } - private suspend inline fun getValueLocked0(): T = + private suspend inline fun getLockedInContext(): T = value ?: initializer!!().also { value = it @@ -164,7 +160,7 @@ internal class SuspendingLazy( } override fun toString(): String = - if (isInitialized) value.toString() else "SuspendingLazy value not initialized yet." + if (value !== null) value.toString() else "SuspendingLazy value not initialized yet." } internal class NullableReference(val ref: T?) { From ae36caba1b6ccdd95bad5ab0616d86fd5524af41 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 8 Dec 2023 11:17:35 -0500 Subject: [PATCH 127/573] QuerySubscription.kt: avoid unnecssarily updating the lastResult flow --- .../kotlin/com/google/firebase/dataconnect/QuerySubscription.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt index 3febf53109f..2b2a26f55f6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt @@ -75,7 +75,7 @@ internal constructor( // clobbering a newer result with an older result, compared using their sequence numbers. while (true) { val oldLastResult = _lastResult.value - if (oldLastResult !== null && oldLastResult.sequenceNumber > newLastResult.sequenceNumber) { + if (oldLastResult !== null && oldLastResult.sequenceNumber >= newLastResult.sequenceNumber) { return } if (_lastResult.compareAndSet(oldLastResult, newLastResult)) { From 34bb6ea24cd7843160ca3467c76ef612c51497f5 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 8 Dec 2023 13:24:58 -0500 Subject: [PATCH 128/573] use Turbine for testing flows --- .../firebase-dataconnect.gradle.kts | 3 +- .../QuerySubscriptionIntegrationTest.kt | 339 ++++++++++++------ .../testutil/schemas/PersonSchema.kt | 5 +- .../firebase/dataconnect/QuerySubscription.kt | 3 +- gradle/libs.versions.toml | 1 + 5 files changed, 235 insertions(+), 116 deletions(-) diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts index 6c0ff6bb10c..cb78f4452a9 100644 --- a/firebase-dataconnect/firebase-dataconnect.gradle.kts +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -116,6 +116,7 @@ dependencies { androidTestImplementation(libs.kotlin.coroutines.test) androidTestImplementation(libs.truth) androidTestImplementation(libs.truth.liteproto.extension) + androidTestImplementation(libs.turbine) } tasks.withType().all { @@ -129,4 +130,4 @@ apply(from = "../gradle/googleServices.gradle") tasks.withType().configureEach { moduleName.set("firebase-dataconnect") -} \ No newline at end of file +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt index f3721ce9abe..ad9923e0e83 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt @@ -17,23 +17,23 @@ package com.google.firebase.dataconnect import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Subject +import app.cash.turbine.test +import app.cash.turbine.turbineScope import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.TestDataConnectFactory -import com.google.firebase.dataconnect.testutil.delayIgnoringTestScheduler -import com.google.firebase.dataconnect.testutil.delayUntil import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation.execute -import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.Data as GetPersonQueryData -import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.Variables as GetPersonQueryVariables +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.execute import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.subscribe +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.update import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute -import java.util.concurrent.CopyOnWriteArrayList import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.* import kotlinx.coroutines.test.* import org.junit.Rule import org.junit.Test @@ -56,148 +56,261 @@ class QuerySubscriptionIntegrationTest { @Test fun lastResult_should_be_equal_to_the_last_collected_result() = runTest { - schema.createPerson.execute(id = "TestId", name = "TestPerson", age = 42) - val querySubscription = schema.getPerson.subscribe(id = "42") - val result = querySubscription.resultFlow.first() - assertThat(querySubscription.lastResult).isEqualTo(result) + schema.createPerson.execute(id = "TestId", name = "Name1") + val querySubscription = schema.getPerson.subscribe(id = "TestId") + + querySubscription.resultFlow.test { + val result1A = awaitItem() + assertWithMessage("result1A.name").that(result1A.data.person?.name).isEqualTo("Name1") + assertWithMessage("lastResult1").that(querySubscription.lastResult).isEqualTo(result1A) + } + + schema.updatePerson.execute(id = "TestId", name = "Name2", age = 2) + + querySubscription.resultFlow.test { + val result1B = awaitItem() + assertWithMessage("result1B").that(result1B).isEqualTo(querySubscription.lastResult) + val result2 = awaitItem() + assertWithMessage("result2.name").that(result2.data.person?.name).isEqualTo("Name2") + assertWithMessage("lastResult2").that(querySubscription.lastResult).isEqualTo(result2) + } } @Test fun reload_should_notify_collecting_flows() = runTest { - schema.createPerson.execute(id = "TestId12345", name = "Name0", age = 10000) - val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - - val resultsChannel = - Channel>( - capacity = Channel.UNLIMITED - ) - backgroundScope.launch { querySubscription.resultFlow.collect(resultsChannel::send) } + schema.createPerson.execute(id = "TestId", name = "Name1") + val querySubscription = schema.getPerson.subscribe(id = "TestId") - val result1 = resultsChannel.receive() - assertThat(result1.data.person).isEqualToGetPersonQueryResult(name = "Name0", age = 10000) + querySubscription.resultFlow.test { + assertWithMessage("result1").that(awaitItem().data.person?.name).isEqualTo("Name1") - schema.updatePerson.execute(id = "TestId12345", name = "Name1", age = 10001) + schema.updatePerson.execute(id = "TestId", name = "Name2") + querySubscription.reload() - querySubscription.reload() - val result2 = resultsChannel.receive() - assertThat(result2.data.person).isEqualToGetPersonQueryResult(name = "Name1", age = 10001) + assertWithMessage("result2").that(awaitItem().data.person?.name).isEqualTo("Name2") + } } @Test fun flow_collect_should_get_immediately_invoked_with_last_result() = runTest { - schema.createPerson.execute(id = "TestId12345", name = "OriginalName", age = 10000) - val querySubscription = schema.getPerson.subscribe(id = "TestId12345") + schema.createPerson.execute(id = "TestId", name = "TestName") + val querySubscription = schema.getPerson.subscribe(id = "TestId") - val result1 = querySubscription.resultFlow.first().data.person - assertWithMessage("result1.name").that(result1!!.name).isEqualTo("OriginalName") + val result1 = querySubscription.resultFlow.first() + assertWithMessage("result1").that(result1.data.person?.name).isEqualTo("TestName") - schema.updatePerson.execute(id = "TestId12345", name = "UpdatedName") - - val result2 = querySubscription.resultFlow.first().data.person - assertWithMessage("result2.name").that(result2!!.name).isEqualTo("OriginalName") + val result2 = querySubscription.resultFlow.first() + assertWithMessage("result2").that(result2.data.person?.name).isEqualTo("TestName") } + @Test + fun flow_collect_should_get_immediately_invoked_with_last_result_from_other_subscribers() = + runTest { + schema.createPerson.execute(id = "TestId", name = "TestName") + val querySubscription1 = schema.getPerson.subscribe(id = "TestId") + val querySubscription2 = schema.getPerson.subscribe(id = "TestId") + + // Start collecting on `querySubscription1` and wait for it to get its first event. + val subscription1ResultReceived = MutableStateFlow(false) + backgroundScope.launch { + querySubscription1.resultFlow.onEach { subscription1ResultReceived.value = true }.collect() + } + subscription1ResultReceived.filter { it }.first() + + // With `querySubscription1` still alive, start collecting on `querySubscription2`. Expect it + // to initially get the cached result from `querySubscription1`, followed by an updated + // result. + schema.updatePerson.execute(id = "TestId", name = "NewTestName") + querySubscription2.resultFlow.test { + assertWithMessage("result1").that(awaitItem().data.person?.name).isEqualTo("TestName") + assertWithMessage("result1").that(awaitItem().data.person?.name).isEqualTo("NewTestName") + } + } + @Test fun slow_flows_do_not_block_fast_flows() = runTest { - schema.createPerson.execute(id = "TestId12345", name = "TestName", age = 10000) - val querySubscription = schema.getPerson.subscribe(id = "TestId12345") + schema.createPerson.execute(id = "TestId", name = "Name0") + val querySubscription = schema.getPerson.subscribe(id = "TestId") - backgroundScope.launch { - querySubscription.resultFlow.collect { delay(Integer.MAX_VALUE.seconds) } - } + turbineScope { + val fastFlow = querySubscription.resultFlow.testIn(backgroundScope) + assertWithMessage("fastFlow").that(fastFlow.awaitItem().data.person?.name).isEqualTo("Name0") + + val slowFlowGate = MutableStateFlow(false) + val slowFlow = + querySubscription.resultFlow + .onEach { slowFlowGate.filter { it }.first() } + .testIn(backgroundScope) + assertWithMessage("fastFlow").that(fastFlow.awaitItem().data.person?.name).isEqualTo("Name0") + + repeat(3) { + schema.updatePerson.execute(id = "TestId", name = "NewName$it") + querySubscription.reload() + } - repeat(5) { - assertWithMessage("fast flow retrieval iteration $it") - .that(querySubscription.resultFlow.first().data.person) - .isEqualToGetPersonQueryResult(name = "TestName", age = 10000) + fastFlow.run { + assertWithMessage("fastFlow").that(awaitItem().data.person?.name).isEqualTo("NewName0") + assertWithMessage("fastFlow").that(awaitItem().data.person?.name).isEqualTo("NewName1") + assertWithMessage("fastFlow").that(awaitItem().data.person?.name).isEqualTo("NewName2") + } + + slowFlowGate.value = true + slowFlow.run { + assertWithMessage("slowFlow").that(awaitItem().data.person?.name).isEqualTo("Name0") + assertWithMessage("slowFlow").that(awaitItem().data.person?.name).isEqualTo("Name0") + assertWithMessage("slowFlow").that(awaitItem().data.person?.name).isEqualTo("NewName0") + assertWithMessage("slowFlow").that(awaitItem().data.person?.name).isEqualTo("NewName1") + assertWithMessage("slowFlow").that(awaitItem().data.person?.name).isEqualTo("NewName2") + } } } @Test - fun reload_delivers_result_to_all_registered_flows() = runTest { - schema.createPerson.execute(id = "TestId12345", name = "TestName0", age = 10000) - val queryVariables = GetPersonQueryVariables(id = "TestId12345") - val querySubscription = schema.getPerson.subscribe(queryVariables) - val results1 = CopyOnWriteArrayList>() - val results2 = CopyOnWriteArrayList>() - backgroundScope.launch { querySubscription.resultFlow.toList(results1) } - delayUntil("results1.isNotEmpty()") { results1.isNotEmpty() } - backgroundScope.launch { querySubscription.resultFlow.toList(results2) } - delayUntil("results2.isNotEmpty()") { results2.isNotEmpty() } - - schema.updatePerson.execute(id = "TestId12345", name = "TestName9", age = 99999) - querySubscription.reload() + fun reload_delivers_result_to_all_registered_flows_on_all_QuerySubscriptions() = runTest { + schema.createPerson.execute(id = "TestId", name = "OriginalName") + val querySubscription1 = schema.getPerson.subscribe(id = "TestId") + val querySubscription2 = schema.getPerson.subscribe(id = "TestId") + + turbineScope { + val flow1a = querySubscription1.resultFlow.testIn(backgroundScope).apply { skipItems(1) } + val flow1b = querySubscription1.resultFlow.testIn(backgroundScope).apply { skipItems(2) } + val flow2 = querySubscription2.resultFlow.testIn(backgroundScope).apply { skipItems(2) } + flow1a.skipItems(2) + flow1b.skipItems(1) + + schema.updatePerson.execute(id = "TestId", name = "NewName") + querySubscription1.reload() + + assertWithMessage("flow1a").that(flow1a.awaitItem().data.person?.name).isEqualTo("NewName") + assertWithMessage("flow1b").that(flow1b.awaitItem().data.person?.name).isEqualTo("NewName") + assertWithMessage("flow2").that(flow2.awaitItem().data.person?.name).isEqualTo("NewName") + } + } - delayIgnoringTestScheduler(2.seconds) - - val expectedResult1 = - DataConnectResult( - variables = queryVariables, - data = GetPersonQueryData(GetPersonQueryData.Person(name = "TestName0", age = 10000)), - sequenceNumber = -1, // sequenceNumber is not considered by equals() - ) - val expectedResult2 = - DataConnectResult( - variables = queryVariables, - data = GetPersonQueryData(GetPersonQueryData.Person(name = "TestName9", age = 99999)), - sequenceNumber = -1, // sequenceNumber is not considered by equals() - ) - assertWithMessage("results1") - .that(results1) - .containsExactly(expectedResult1, expectedResult1, expectedResult2) - .inOrder() - assertWithMessage("results2") - .that(results2) - .containsExactly(expectedResult1, expectedResult1, expectedResult2) - .inOrder() + @Test + fun queryref_execute_delivers_result_to_QuerySubscriptions() = runTest { + schema.createPerson.execute(id = "TestId", name = "OriginalName") + val querySubscription1 = schema.getPerson.subscribe(id = "TestId") + val querySubscription2 = schema.getPerson.subscribe(id = "TestId") + + turbineScope { + val flow1a = querySubscription1.resultFlow.testIn(backgroundScope).apply { skipItems(1) } + val flow1b = querySubscription1.resultFlow.testIn(backgroundScope).apply { skipItems(2) } + val flow2 = querySubscription2.resultFlow.testIn(backgroundScope).apply { skipItems(2) } + flow1a.skipItems(2) + flow1b.skipItems(1) + + schema.updatePerson.execute(id = "TestId", name = "NewName") + schema.getPerson.execute(id = "TestId") + + assertWithMessage("flow1a").that(flow1a.awaitItem().data.person?.name).isEqualTo("NewName") + assertWithMessage("flow1b").that(flow1b.awaitItem().data.person?.name).isEqualTo("NewName") + assertWithMessage("flow2").that(flow2.awaitItem().data.person?.name).isEqualTo("NewName") + } } @Test fun reload_concurrent_invocations_get_conflated() = runTest(timeout = 60.seconds) { - schema.createPerson.execute(id = "TestId12345", name = "Name", age = 10000) - val querySubscription = schema.getPerson.subscribe(id = "TestId12345") - - val resultCollected = MutableStateFlow(false) - val collectedResults = - CopyOnWriteArrayList>() - backgroundScope.launch { - querySubscription.resultFlow - .onEach { resultCollected.value = true } - .toList(collectedResults) + schema.createPerson.execute(id = "TestId", name = "OriginalName") + val querySubscription = schema.getPerson.subscribe(id = "TestId") + + querySubscription.resultFlow.test { + assertThat(awaitItem().data.person?.name).isEqualTo("OriginalName") + schema.updatePerson.execute(id = "TestId", name = "NewName") + + buildList { + repeat(25_000) { + // Run on Dispatchers.Default to ensure some level of concurrency. + add(backgroundScope.async(Dispatchers.Default) { querySubscription.reload() }) + } + } + .forEach { it.await() } + + // Flow on Dispatchers.Default so that the timeout actually works, since the default + // dispatcher is the _test_ dispatcher, which skips delays/timeouts. + val results = + asChannel() + .receiveAsFlow() + .timeout(1.seconds) + .flowOn(Dispatchers.Default) + .catch { if (it !is TimeoutCancellationException) throw it } + .toList() + assertWithMessage("results.size").that(results.size).isGreaterThan(0) + assertWithMessage("results.size").that(results.size).isLessThan(1000) + results.forEachIndexed { i, result -> + assertWithMessage("results[$i]").that(result.data.person?.name).isEqualTo("NewName") + } } + } - val deferreds = buildList { - repeat(25_000) { - // Use `Dispatchers.Default` as the dispatcher for the launched coroutines so that there - // will be at least 2 threads used to run the coroutines (as documented by - // `Dispatchers.Default`), introducing a guaranteed minimum level of parallelism, ensuring - // that this test is indeed testing "massive concurrency". - add(backgroundScope.async(Dispatchers.Default) { querySubscription.reload() }) - } + @Test + fun update_changes_variables_and_triggers_reload() = runTest { + schema.createPerson.execute(id = "TestId1", name = "Name1") + schema.createPerson.execute(id = "TestId2", name = "Name2") + schema.createPerson.execute(id = "TestId3", name = "Name3") + val querySubscription = schema.getPerson.subscribe(id = "TestId1") + + querySubscription.resultFlow.test { + Pair(assertWithMessage("result1"), awaitItem()).let { (assert, result) -> + assert.that(result.variables).isEqualTo(GetPersonQuery.Variables("TestId1")) + assert.that(result.data.person?.name).isEqualTo("Name1") + } + querySubscription.update("TestId2") + Pair(assertWithMessage("result2"), awaitItem()).let { (assert, result) -> + assert.that(result.variables).isEqualTo(GetPersonQuery.Variables("TestId2")) + assert.that(result.data.person?.name).isEqualTo("Name2") + } + querySubscription.update("TestId3") + Pair(assertWithMessage("result3"), awaitItem()).let { (assert, result) -> + assert.that(result.variables).isEqualTo(GetPersonQuery.Variables("TestId3")) + assert.that(result.data.person?.name).isEqualTo("Name3") } + } + } - // Wait for at least one result to come in. - resultCollected.filter { it }.first() + @Test + fun reload_updates_last_result_even_if_no_active_collectors() = runTest { + schema.createPerson.execute(id = "TestId", name = "Name1") + val querySubscription = schema.getPerson.subscribe(id = "TestId") - // Wait for all calls to reload() to complete. - deferreds.forEach { it.await() } + querySubscription.reload() - // Verify that we got the expected results. - var collectedResultsIndex = 0 - while (collectedResultsIndex < collectedResults.size) { - assertWithMessage("collectedResults[$collectedResultsIndex]") - .that(collectedResults[collectedResultsIndex].data.person) - .isEqualToGetPersonQueryResult(name = "Name", age = 10000) - collectedResultsIndex++ - yield() - } + Pair(assertWithMessage("lastResult"), querySubscription.lastResult).let { (assert, lastResult) + -> + assert.that(lastResult).isNotNull() + assert.that(lastResult!!.data.person?.name).isEqualTo("Name1") + } - // Verify that the calls to reload() were conflated, by ensuring that we got less than - // 25,000 results. - assertWithMessage("collectedResultsIndex").that(collectedResultsIndex).isLessThan(10000) + schema.updatePerson.execute(id = "TestId", name = "Name2") + querySubscription.resultFlow.test { + // Ensure that the first result comes from cache, followed by the updated result received from + // the server when a reload was triggered by the flow's collection. + assertThat(awaitItem().data.person?.name).isEqualTo("Name1") + assertThat(awaitItem().data.person?.name).isEqualTo("Name2") } -} + } + + @Test + fun update_updates_last_result_even_if_no_active_collectors() = runTest { + schema.createPerson.execute(id = "TestId1", name = "Name1") + schema.createPerson.execute(id = "TestId2", name = "Name2") + val querySubscription = schema.getPerson.subscribe(id = "TestId1") -private fun Subject.isEqualToGetPersonQueryResult(name: String, age: Int?) = - isEqualTo(GetPersonQueryData.Person(name = name, age = age)) + querySubscription.update("TestId2") + + Pair(assertWithMessage("lastResult"), querySubscription.lastResult).let { (assert, lastResult) + -> + assert.that(lastResult).isNotNull() + assert.that(lastResult!!.data.person?.name).isEqualTo("Name2") + } + + schema.updatePerson.execute(id = "TestId2", name = "NewName2") + querySubscription.resultFlow.test { + // Ensure that the first result comes from cache, followed by the updated result received from + // the server when a reload was triggered by the flow's collection. + assertThat(awaitItem().data.person?.name).isEqualTo("Name2") + assertThat(awaitItem().data.person?.name).isEqualTo("NewName2") + } + } +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index 8f908277047..9248f96840c 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -17,6 +17,7 @@ package com.google.firebase.dataconnect.testutil.schemas import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.MutationRef import com.google.firebase.dataconnect.QueryRef +import com.google.firebase.dataconnect.QuerySubscription import com.google.firebase.dataconnect.mutation import com.google.firebase.dataconnect.query import com.google.firebase.dataconnect.testutil.TestDataConnectFactory @@ -53,7 +54,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { @Serializable data class PersonData(val id: String, val name: String, val age: Int? = null) @Serializable data class Variables(val data: PersonData) - suspend fun MutationRef.execute(id: String, name: String, age: Int?) = + suspend fun MutationRef.execute(id: String, name: String, age: Int? = null) = execute(Variables(PersonData(id = id, name = name, age = age))) } @@ -106,6 +107,8 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { suspend fun QueryRef.execute(id: String) = execute(Variables(id = id)) fun QueryRef.subscribe(id: String) = subscribe(Variables(id = id)) + + suspend fun QuerySubscription.update(id: String) = update(Variables(id = id)) } val getPerson = diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt index 2b2a26f55f6..4ba640bddfe 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QuerySubscription.kt @@ -62,7 +62,8 @@ internal constructor( suspend fun reload() { val queryManager = query.dataConnect.lazyQueryManager.get() - queryManager.execute(query, variables) + val result = queryManager.execute(query, variables) + updateLastResult(result) } suspend fun update(variables: VariablesType) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6eb5e906ab3..a4a364d1641 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,6 +78,7 @@ kotest-runner = { module = "io.kotest:kotest-runner-junit4-jvm", version.ref = " kotest-assertions = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" } kotest-property = { module = "io.kotest:kotest-property-jvm", version.ref = "kotest" } quickcheck = { module = "net.java:quickcheck", version.ref = "quickcheck" } +turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" } [bundles] kotest = ["kotest-runner", "kotest-assertions", "kotest-property"] From fc0b11190c524d1869409cfb60b117f164f93f0b Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 8 Dec 2023 15:48:10 -0500 Subject: [PATCH 129/573] QueryManager.kt: fix bug where latest result is not delivered for a new data deserializer --- .../QuerySubscriptionIntegrationTest.kt | 159 ++++++++++++++++++ .../dataconnect/testutil/TurbineUtils.kt | 12 ++ .../firebase/dataconnect/QueryManager.kt | 29 +++- 3 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TurbineUtils.kt diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt index ad9923e0e83..95f5daa7399 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/QuerySubscriptionIntegrationTest.kt @@ -29,12 +29,20 @@ import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQu import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.subscribe import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery.update import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.UpdatePersonMutation.execute +import com.google.firebase.dataconnect.testutil.skipItemsWhere import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.* import kotlinx.coroutines.test.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.serializer import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -313,4 +321,155 @@ class QuerySubscriptionIntegrationTest { assertThat(awaitItem().data.person?.name).isEqualTo("NewName2") } } + + @Test + fun collect_does_not_get_an_update_on_errors() = runTest { + schema.createPerson.execute(id = "TestId", name = "Name1") + val testIdVariables = GetPersonQuery.Variables("TestId") + + val noName2Query = schema.getPerson.withDataDeserializer(serializer()) + + turbineScope { + val querySubscription = noName2Query.subscribe(testIdVariables) + val flow = querySubscription.resultFlow.testIn(backgroundScope) + assertThat(flow.awaitItem().data.person?.name).isEqualTo("Name1") + + schema.updatePerson.execute(id = "TestId", name = "Name2") + val result2 = querySubscription.runCatching { reload() } + assertWithMessage("result2.isSuccess").that(result2.isSuccess).isFalse() + + schema.updatePerson.execute(id = "TestId", name = "Name3") + querySubscription.reload() + assertThat(flow.awaitItem().data.person?.name).isEqualTo("Name3") + } + } + + @Test + fun collect_gets_notified_of_per_data_deserializer_successes() = runTest { + schema.createPerson.execute(id = "TestId", name = "Name0") + val testIdVariables = GetPersonQuery.Variables("TestId") + + val noName1Query = schema.getPerson.withDataDeserializer(serializer()) + val noName2Query = schema.getPerson.withDataDeserializer(serializer()) + + turbineScope { + val flow1 = noName1Query.subscribe(testIdVariables).resultFlow.testIn(backgroundScope) + assertThat(flow1.awaitItem().data.person?.name).isEqualTo("Name0") + val flow2 = noName2Query.subscribe(testIdVariables).resultFlow.testIn(backgroundScope) + assertThat(flow1.awaitItem().data.person?.name).isEqualTo("Name0") + + schema.updatePerson.execute(id = "TestId", name = "Name1") + schema.getPerson.execute(testIdVariables) + flow2 + .skipItemsWhere { it.data.person?.name == "Name0" } + .let { assertThat(it.data.person?.name).isEqualTo("Name1") } + + schema.updatePerson.execute(id = "TestId", name = "Name2") + schema.getPerson.execute(testIdVariables) + flow1 + .skipItemsWhere { it.data.person?.name == "Name0" } + .let { assertThat(it.data.person?.name).isEqualTo("Name2") } + + schema.updatePerson.execute(id = "TestId", name = "Name3") + schema.getPerson.execute(testIdVariables) + assertThat(flow1.awaitItem().data.person?.name).isEqualTo("Name3") + assertThat(flow2.awaitItem().data.person?.name).isEqualTo("Name3") + } + } + + @Test + fun collect_gets_notified_of_previous_cached_success_even_if_most_recent_fails() = runTest { + schema.createPerson.execute(id = "TestId", name = "OriginalName") + val testIdVariables = GetPersonQuery.Variables("TestId") + keepCacheAlive(schema.getPerson.withDataDeserializer(DataConnectUntypedData), testIdVariables) + + val noName1Query = schema.getPerson.withDataDeserializer(serializer()) + noName1Query.execute(testIdVariables) + + schema.updatePerson.execute(id = "TestId", name = "Name1") + + noName1Query.subscribe(testIdVariables).resultFlow.test { + assertWithMessage("result1").that(awaitItem().data.person?.name).isEqualTo("OriginalName") + schema.updatePerson.execute(id = "TestId", name = "UltimateName") + schema.getPerson.execute(testIdVariables) + assertWithMessage("result2").that(awaitItem().data.person?.name).isEqualTo("UltimateName") + } + } + + @Test + fun collect_gets_cached_result_even_if_new_data_deserializer() = runTest { + schema.createPerson.execute(id = "TestId", name = "OriginalName") + val testIdVariables = GetPersonQuery.Variables("TestId") + keepCacheAlive(schema.getPerson.withDataDeserializer(DataConnectUntypedData), testIdVariables) + + schema.updatePerson.execute(id = "TestId", name = "UltimateName") + + schema.getPerson.subscribe(testIdVariables).resultFlow.test { + assertWithMessage("result1").that(awaitItem().data.person?.name).isEqualTo("OriginalName") + assertWithMessage("result2").that(awaitItem().data.person?.name).isEqualTo("UltimateName") + } + } + + private sealed class RejectSpecificNameKSerializer(val nameToReject: String) : + KSerializer { + override val descriptor = PrimitiveSerialDescriptor("name", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder) = + decoder.decodeString().also { + if (it == nameToReject) { + throw RejectedName("name rejected: $it") + } + } + + override fun serialize(encoder: Encoder, value: String) { + throw UnsupportedOperationException("") + } + + class RejectedName(message: String) : Exception(message) + } + + /** + * A "data" type suitable for the [GetPersonQuery] whose deserialization fails if the name happens + * to be "Name1". This behavior is useful when testing the caching behavior when one deserializer + * successfully decodes a response but another one does not. See [GetPersonDataNoName2]. + */ + @Serializable + private data class GetPersonDataNoName1(val person: Person?) { + @Serializable + data class Person( + @Serializable(with = NameKSerializer::class) val name: String, + val age: Int? + ) { + private object NameKSerializer : RejectSpecificNameKSerializer("Name1") + } + } + + /** + * A "data" type suitable for the [GetPersonQuery] whose deserialization fails if the name happens + * to be "Name2". This behavior is useful when testing the caching behavior when one deserializer + * successfully decodes a response but another one does not. See [GetPersonDataNoName1]. + */ + @Serializable + private data class GetPersonDataNoName2(val person: Person?) { + @Serializable + data class Person( + @Serializable(with = NameKSerializer::class) val name: String, + val age: Int? + ) { + private object NameKSerializer : RejectSpecificNameKSerializer("Name2") + } + } + + /** + * Starts a background coroutine that subscribes to and collects the given query with the given + * variables. Suspends until the first result has been collected. This effectively ensures that + * the cache for the query with the given variables never gets garbage collected. + */ + private suspend fun TestScope.keepCacheAlive(query: QueryRef, variables: V) { + val cachePrimed = MutableStateFlow(false) + backgroundScope.launch { + query.subscribe(variables).resultFlow.onEach { cachePrimed.value = true }.collect() + } + cachePrimed.filter { it }.first() + } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TurbineUtils.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TurbineUtils.kt new file mode 100644 index 00000000000..898a7935ffe --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TurbineUtils.kt @@ -0,0 +1,12 @@ +package com.google.firebase.dataconnect.testutil + +import app.cash.turbine.ReceiveTurbine + +suspend fun ReceiveTurbine.skipItemsWhere(predicate: (T) -> Boolean): T { + while (true) { + val item = awaitItem() + if (!predicate(item)) { + return item + } + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt index c561f44ca34..a65cb9df317 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/QueryManager.kt @@ -94,6 +94,15 @@ private class LiveQuery( // read-write-modify operations can be done atomically. private val dataDeserializersWriteMutex = Mutex() private val dataDeserializers = CopyOnWriteArrayList>() + private data class Update( + val requestId: String, + val sequenceNumber: Long, + val result: Result + ) + // Also, `initialDataDeserializerUpdate` must only be accessed while `dataDeserializersWriteMutex` + // is held. + private val initialDataDeserializerUpdate = + MutableStateFlow>(NullableReference(null)) private val jobMutex = Mutex() private var job: Job? = null @@ -182,6 +191,19 @@ private class LiveQuery( ) } + // Normally, setting the value of `initialDataDeserializerUpdate` would be done in a compare- + // and-swap ("CAS") loop to avoid clobbering a newer update with an older one; however, since + // all writes _must_ be done by a coroutine with `dataDeserializersWriteMutex` locked, the CAS + // loop isn't necessary and its value can just be set directly. + dataDeserializersWriteMutex.withLock { + initialDataDeserializerUpdate.value.ref.let { oldUpdate -> + if (oldUpdate === null || oldUpdate.sequenceNumber < sequenceNumber) { + initialDataDeserializerUpdate.value = + NullableReference(Update(requestId, sequenceNumber, executeQueryResult)) + } + } + } + dataDeserializers.iterator().forEach { it.update(requestId, sequenceNumber, executeQueryResult) } @@ -212,7 +234,12 @@ private class LiveQuery( blockingDispatcher = blockingDispatcher, parentLogger = logger ) - .also { dataDeserializers.add(it) } + .also { + dataDeserializers.add(it) + initialDataDeserializerUpdate.value.ref?.run { + it.update(requestId, sequenceNumber, result) + } + } } } From 1bc15fd7bbe1f34eef3def6e466f21c30efd5869 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 24 Jan 2024 15:42:07 -0500 Subject: [PATCH 130/573] update protos --- .../scripts/emulator/firemat.yaml | 2 +- .../FirebaseDataConnectIntegrationTest.kt | 136 ++---------------- .../testutil/EmulatorController.kt | 8 +- .../testutil/TestDataConnectFactory.kt | 18 +-- .../testutil/schemas/AllTypesSchema.kt | 10 +- .../testutil/schemas/PersonSchema.kt | 10 +- .../{v1main => }/emulator_service.proto | 11 +- .../dataconnect/DataConnectGrpcClient.kt | 12 +- .../dataconnect/FirebaseDataConnect.kt | 35 ++--- .../dataconnect/FirebaseDataConnectFactory.kt | 23 +-- .../google/firebase/dataconnect/ProtoUtil.kt | 8 +- .../firebase/dataconnect/generated/Posts.kt | 3 +- .../v1main/connector_service.proto | 91 ++++++++++++ .../dataconnect/v1main/data_service.proto | 101 ------------- .../dataconnect/v1main/graphql_error.proto | 71 +++++++++ 15 files changed, 229 insertions(+), 310 deletions(-) rename firebase-dataconnect/src/androidTest/proto/google/firebase/dataconnect/emulator/{v1main => }/emulator_service.proto (84%) create mode 100644 firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/connector_service.proto delete mode 100644 firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/data_service.proto create mode 100644 firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/graphql_error.proto diff --git a/firebase-dataconnect/scripts/emulator/firemat.yaml b/firebase-dataconnect/scripts/emulator/firemat.yaml index 60d04ecaac9..8aab9b91dd6 100644 --- a/firebase-dataconnect/scripts/emulator/firemat.yaml +++ b/firebase-dataconnect/scripts/emulator/firemat.yaml @@ -2,6 +2,6 @@ specVersion: v1alpha schema: main: source: ./api/schema -operationSet: +connector: crud: source: ./api/operations diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt index 34e859ae6a6..e6821b0e54a 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectIntegrationTest.kt @@ -107,30 +107,15 @@ class FirebaseDataConnectIntegrationTest { } @Test - fun getInstance_should_return_distinct_instances_for_distinct_operationSets() { + fun getInstance_should_return_distinct_instances_for_distinct_connectors() { val nonDefaultApp = firebaseAppFactory.newInstance() - val serviceConfig1 = SAMPLE_SERVICE_CONFIG1.withOperationSet("foo") - val serviceConfig2 = serviceConfig1.withOperationSet("bar") + val serviceConfig1 = SAMPLE_SERVICE_CONFIG1.withConnector("foo") + val serviceConfig2 = serviceConfig1.withConnector("bar") val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig1) val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig2) assertThat(instance1).isNotSameInstanceAs(instance2) } - @Test - fun getInstance_should_throw_if_revision_differs_from_that_of_cached_instance() { - val nonDefaultApp = firebaseAppFactory.newInstance() - val serviceConfig1 = SAMPLE_SERVICE_CONFIG1.withRevision("foo") - val serviceConfig2 = serviceConfig1.withRevision("bar") - val instance1 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig1) - - assertThrows(IllegalArgumentException::class.java) { - FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig2) - } - - val instance2 = FirebaseDataConnect.getInstance(nonDefaultApp, serviceConfig1) - assertThat(instance1).isSameInstanceAs(instance2) - } - @Test fun getInstance_should_return_a_new_instance_after_the_instance_is_terminated() { val nonDefaultApp = firebaseAppFactory.newInstance() @@ -227,17 +212,6 @@ class FirebaseDataConnectIntegrationTest { assertThat(instance1).isNotSameInstanceAs(instance2) } - @Test - fun getInstance_should_allow_different_revision_after_first_instance_is_closed() { - val nonDefaultApp = firebaseAppFactory.newInstance() - val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp, SAMPLE_SERVICE_CONFIG1.withRevision("foo")) - instance1.close() - val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp, SAMPLE_SERVICE_CONFIG1.withRevision("bar")) - assertThat(instance1).isNotSameInstanceAs(instance2) - } - @Test fun getInstance_should_return_new_instance_if_settings_and_app_are_both_different() { val nonDefaultApp1 = firebaseAppFactory.newInstance() @@ -257,17 +231,6 @@ class FirebaseDataConnectIntegrationTest { assertThat(instance1).isNotSameInstanceAs(instance2) } - @Test - fun getInstance_should_return_new_instance_if_revision_and_app_are_both_different() { - val nonDefaultApp1 = firebaseAppFactory.newInstance() - val nonDefaultApp2 = firebaseAppFactory.newInstance() - val instance1 = - FirebaseDataConnect.getInstance(nonDefaultApp1, SAMPLE_SERVICE_CONFIG1.withRevision("foo")) - val instance2 = - FirebaseDataConnect.getInstance(nonDefaultApp2, SAMPLE_SERVICE_CONFIG1.withRevision("bar")) - assertThat(instance1).isNotSameInstanceAs(instance2) - } - @Test fun getInstance_should_return_new_instance_if_settings_and_serviceId_are_both_different() { val nonDefaultApp = firebaseAppFactory.newInstance() @@ -291,25 +254,6 @@ class FirebaseDataConnectIntegrationTest { .isEqualTo(FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2")) } - @Test - fun getInstance_should_return_new_instance_if_revision_and_serviceId_are_both_different() { - val nonDefaultApp = firebaseAppFactory.newInstance() - val instance1 = - FirebaseDataConnect.getInstance( - nonDefaultApp, - SAMPLE_SERVICE_CONFIG1.withServiceId("foo").withRevision("boo") - ) - val instance2 = - FirebaseDataConnect.getInstance( - nonDefaultApp, - SAMPLE_SERVICE_CONFIG1.withServiceId("bar").withRevision("zoo") - ) - - assertThat(instance1).isNotSameInstanceAs(instance2) - assertThat(instance1.serviceConfig.revision).isEqualTo("boo") - assertThat(instance2.serviceConfig.revision).isEqualTo("zoo") - } - @Test fun getInstance_should_return_new_instance_if_settings_and_location_are_both_different() { val nonDefaultApp = firebaseAppFactory.newInstance() @@ -333,25 +277,6 @@ class FirebaseDataConnectIntegrationTest { .isEqualTo(FirebaseDataConnectSettings.defaults.copy(hostName = "TestHostName2")) } - @Test - fun getInstance_should_return_new_instance_if_revision_and_location_are_both_different() { - val nonDefaultApp = firebaseAppFactory.newInstance() - val instance1 = - FirebaseDataConnect.getInstance( - nonDefaultApp, - SAMPLE_SERVICE_CONFIG1.withLocation("foo").withRevision("boo") - ) - val instance2 = - FirebaseDataConnect.getInstance( - nonDefaultApp, - SAMPLE_SERVICE_CONFIG1.withLocation("bar").withRevision("zoo") - ) - - assertThat(instance1).isNotSameInstanceAs(instance2) - assertThat(instance1.serviceConfig.revision).isEqualTo("boo") - assertThat(instance2.serviceConfig.revision).isEqualTo("zoo") - } - @Test fun getInstance_should_be_thread_safe() { val apps = @@ -415,8 +340,7 @@ class FirebaseDataConnectIntegrationTest { ServiceConfig( serviceId = "TestServiceId", location = "TestLocation", - operationSet = "TestOperationSet", - revision = "TestRevision" + connector = "TestConnector" ) ) @@ -426,75 +350,45 @@ class FirebaseDataConnectIntegrationTest { assertThat(toStringResult).containsWithNonAdjacentText("projectId=${app.options.projectId}") assertThat(toStringResult).containsWithNonAdjacentText("serviceId=TestServiceId") assertThat(toStringResult).containsWithNonAdjacentText("location=TestLocation") - assertThat(toStringResult).containsWithNonAdjacentText("operationSet=TestOperationSet") - assertThat(toStringResult).containsWithNonAdjacentText("revision=TestRevision") + assertThat(toStringResult).containsWithNonAdjacentText("connector=TestConnector") } } private val SAMPLE_SERVICE_ID1 = "SampleServiceId1" private val SAMPLE_LOCATION1 = "SampleLocation1" -private val SAMPLE_OPERATION_SET1 = "SampleOperationSet1" -private val SAMPLE_REVISION1 = "SampleRevision1" +private val SAMPLE_CONNECTOR1 = "SampleConnector1" private val SAMPLE_SERVICE_CONFIG1 = ServiceConfig( serviceId = SAMPLE_SERVICE_ID1, location = SAMPLE_LOCATION1, - operationSet = SAMPLE_OPERATION_SET1, - revision = SAMPLE_REVISION1 + connector = SAMPLE_CONNECTOR1 ) private val SAMPLE_SERVICE_ID2 = "SampleServiceId2" private val SAMPLE_LOCATION2 = "SampleLocation2" -private val SAMPLE_OPERATION_SET2 = "SampleOperationSet2" -private val SAMPLE_REVISION2 = "SampleRevision2" +private val SAMPLE_CONNECTOR2 = "SampleConnector2" private val SAMPLE_SERVICE_CONFIG2 = ServiceConfig( serviceId = SAMPLE_SERVICE_ID2, location = SAMPLE_LOCATION2, - operationSet = SAMPLE_OPERATION_SET2, - revision = SAMPLE_REVISION2 + connector = SAMPLE_CONNECTOR2 ) private val SAMPLE_SERVICE_ID3 = "SampleServiceId3" private val SAMPLE_LOCATION3 = "SampleLocation3" -private val SAMPLE_OPERATION_SET3 = "SampleOperationSet3" -private val SAMPLE_REVISION3 = "SampleRevision3" +private val SAMPLE_CONNECTOR3 = "SampleConnector3" private val SAMPLE_SERVICE_CONFIG3 = ServiceConfig( serviceId = SAMPLE_SERVICE_ID3, location = SAMPLE_LOCATION3, - operationSet = SAMPLE_OPERATION_SET3, - revision = SAMPLE_REVISION3 + connector = SAMPLE_CONNECTOR3 ) private fun ServiceConfig.withServiceId(newServiceId: String) = - ServiceConfig( - serviceId = newServiceId, - location = location, - operationSet = operationSet, - revision = revision, - ) + ServiceConfig(serviceId = newServiceId, location = location, connector = connector) private fun ServiceConfig.withLocation(newLocation: String) = - ServiceConfig( - serviceId = serviceId, - location = newLocation, - operationSet = operationSet, - revision = revision, - ) + ServiceConfig(serviceId = serviceId, location = newLocation, connector = connector) -private fun ServiceConfig.withOperationSet(newOperationSet: String) = - ServiceConfig( - serviceId = serviceId, - location = location, - operationSet = newOperationSet, - revision = revision, - ) - -private fun ServiceConfig.withRevision(newRevision: String) = - ServiceConfig( - serviceId = serviceId, - location = location, - operationSet = operationSet, - revision = newRevision, - ) +private fun ServiceConfig.withConnector(newConnector: String) = + ServiceConfig(serviceId = serviceId, location = location, connector = newConnector) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt index 138c14399f0..51feb1ffb14 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/EmulatorController.kt @@ -4,10 +4,10 @@ import android.content.res.AssetManager import com.google.firebase.Firebase import com.google.firebase.app import com.google.firebase.dataconnect.FirebaseDataConnect -import google.firebase.dataconnect.emulator.v1main.EmulatorServiceGrpcKt.EmulatorServiceCoroutineStub -import google.firebase.dataconnect.emulator.v1main.file -import google.firebase.dataconnect.emulator.v1main.setupSchemaRequest -import google.firebase.dataconnect.emulator.v1main.source +import google.firebase.dataconnect.emulator.EmulatorServiceGrpcKt.EmulatorServiceCoroutineStub +import google.firebase.dataconnect.emulator.file +import google.firebase.dataconnect.emulator.setupSchemaRequest +import google.firebase.dataconnect.emulator.source import io.grpc.ManagedChannelBuilder import io.grpc.android.AndroidChannelBuilder import java.io.InputStreamReader diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt index 7639f1ce394..b2ae34052f4 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/TestDataConnectFactory.kt @@ -37,17 +37,9 @@ class TestDataConnectFactory : fun newInstance( serviceId: String? = null, location: String? = null, - operationSet: String? = null, - revision: String? = null + connector: String? = null ): FirebaseDataConnect = - newInstance( - Params( - serviceId = serviceId, - location = location, - operationSet = operationSet, - revision = revision - ) - ) + newInstance(Params(serviceId = serviceId, location = location, connector = connector)) override fun createInstance(params: Params?): FirebaseDataConnect { val instanceId = Random.nextAlphanumericString() @@ -55,8 +47,7 @@ class TestDataConnectFactory : FirebaseDataConnect.ServiceConfig( serviceId = params?.serviceId ?: "TestService$instanceId", location = params?.location ?: "TestLocation$instanceId", - operationSet = params?.operationSet ?: "TestOperationSet$instanceId", - revision = params?.revision ?: "TestRevision$instanceId" + connector = params?.connector ?: "TestConnector$instanceId" ) return FirebaseDataConnect.getInstance(serviceConfig, FirebaseDataConnectSettings.emulator) } @@ -68,7 +59,6 @@ class TestDataConnectFactory : data class Params( val location: String? = null, val serviceId: String? = null, - val operationSet: String? = null, - val revision: String? = null + val connector: String? = null ) } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt index 29e63120585..ba76e639755 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/AllTypesSchema.kt @@ -26,9 +26,9 @@ import kotlinx.serialization.serializer class AllTypesSchema(val dataConnect: FirebaseDataConnect) { init { - dataConnect.serviceConfig.operationSet.let { - require(it == OPERATION_SET) { - "The given FirebaseDataConnect has operationSet=$it, but expected $OPERATION_SET" + dataConnect.serviceConfig.connector.let { + require(it == CONNECTOR) { + "The given FirebaseDataConnect has operationSet=$it, but expected $CONNECTOR" } } } @@ -250,9 +250,9 @@ class AllTypesSchema(val dataConnect: FirebaseDataConnect) { suspend fun getFarm(id: String) = getFarm.execute(GetFarmQuery.Variables(id = id)) companion object { - const val OPERATION_SET = "ops" + const val CONNECTOR = "ops" suspend fun TestDataConnectFactory.installAllTypesSchema(): AllTypesSchema = - AllTypesSchema(newInstance(operationSet = OPERATION_SET)).apply { installEmulatorSchema() } + AllTypesSchema(newInstance(connector = CONNECTOR)).apply { installEmulatorSchema() } } } diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index 9248f96840c..977cb61e687 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -28,9 +28,9 @@ import kotlinx.serialization.serializer class PersonSchema(val dataConnect: FirebaseDataConnect) { init { - dataConnect.serviceConfig.operationSet.let { - require(it == OPERATION_SET) { - "The given FirebaseDataConnect has operationSet=$it, but expected $OPERATION_SET" + dataConnect.serviceConfig.connector.let { + require(it == CONNECTOR) { + "The given FirebaseDataConnect has operationSet=$it, but expected $CONNECTOR" } } } @@ -137,9 +137,9 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { ) companion object { - const val OPERATION_SET = "ops" + const val CONNECTOR = "ops" suspend fun TestDataConnectFactory.installPersonSchema(): PersonSchema = - PersonSchema(newInstance(operationSet = OPERATION_SET)).apply { installEmulatorSchema() } + PersonSchema(newInstance(connector = CONNECTOR)).apply { installEmulatorSchema() } } } diff --git a/firebase-dataconnect/src/androidTest/proto/google/firebase/dataconnect/emulator/v1main/emulator_service.proto b/firebase-dataconnect/src/androidTest/proto/google/firebase/dataconnect/emulator/emulator_service.proto similarity index 84% rename from firebase-dataconnect/src/androidTest/proto/google/firebase/dataconnect/emulator/v1main/emulator_service.proto rename to firebase-dataconnect/src/androidTest/proto/google/firebase/dataconnect/emulator/emulator_service.proto index 93a2a7d0d33..8d16a0fed11 100644 --- a/firebase-dataconnect/src/androidTest/proto/google/firebase/dataconnect/emulator/v1main/emulator_service.proto +++ b/firebase-dataconnect/src/androidTest/proto/google/firebase/dataconnect/emulator/emulator_service.proto @@ -1,14 +1,17 @@ -// Adapted from http://google3/firebase/firemat/emulator/server/api/emulator_service.proto;rcl=578315096 +// Adapted from http://google3/third_party/firebase/dataconnect/emulator/server/api/emulator_service.proto;rcl=597032027 // API protos for the FireMAT Emulator Service. syntax = "proto3"; -package google.firebase.dataconnect.emulator.v1main; +package google.firebase.dataconnect.emulator; -import "google/firebase/dataconnect/v1main/data_service.proto"; +import "google/firebase/dataconnect/v1main/connector_service.proto"; +import "google/firebase/dataconnect/v1main/graphql_error.proto"; import "google/protobuf/empty.proto"; +option java_multiple_files = true; + service EmulatorService { // Surfaces GQL parsing errors. rpc GetCompileErrors(GetCompileErrorsRequest) returns (GetCompileErrorsResponse) { @@ -30,7 +33,7 @@ message GetCompileErrorsRequest { } message GetCompileErrorsResponse { - repeated google.firebase.dataconnect.v1main.GraphqlError errors = 1; + repeated GraphqlError errors = 1; } message SetupSchemaRequest { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt index 039260cd0cd..64ff7a2ace5 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectGrpcClient.kt @@ -18,8 +18,8 @@ import com.google.android.gms.security.ProviderInstaller import com.google.protobuf.ListValue import com.google.protobuf.Struct import com.google.protobuf.Value -import google.firebase.dataconnect.v1main.DataServiceGrpcKt.DataServiceCoroutineStub -import google.firebase.dataconnect.v1main.DataServiceOuterClass.GraphqlError +import google.firebase.dataconnect.v1main.ConnectorServiceGrpcKt.ConnectorServiceCoroutineStub +import google.firebase.dataconnect.v1main.GraphqlError import google.firebase.dataconnect.v1main.executeMutationRequest import google.firebase.dataconnect.v1main.executeQueryRequest import io.grpc.ManagedChannel @@ -37,8 +37,7 @@ internal class DataConnectGrpcClient( projectId: String, serviceId: String, location: String, - operationSet: String, - revision: String, + connector: String, hostName: String, port: Int, sslEnabled: Boolean, @@ -49,8 +48,7 @@ internal class DataConnectGrpcClient( Logger("DataConnectGrpcClient").apply { debug { "Created by ${parentLogger.nameWithId}" } } private val requestName = - "projects/$projectId/locations/$location/services/$serviceId/" + - "operationSets/$operationSet/revisions/$revision" + "projects/$projectId/locations/$location/services/$serviceId/connectors/$connector" private val closedMutex = Mutex() private var closed = false @@ -99,7 +97,7 @@ internal class DataConnectGrpcClient( channel } - private val lazyGrpcStub = SuspendingLazy { DataServiceCoroutineStub(lazyGrpcChannel.get()) } + private val lazyGrpcStub = SuspendingLazy { ConnectorServiceCoroutineStub(lazyGrpcChannel.get()) } data class OperationResult( val data: Struct?, diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 93a6369fffa..a555fba8b57 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -77,8 +77,7 @@ internal constructor( projectId = projectId, serviceId = serviceConfig.serviceId, location = serviceConfig.location, - revision = serviceConfig.revision, - operationSet = serviceConfig.operationSet, + connector = serviceConfig.connector, hostName = settings.hostName, port = settings.port, sslEnabled = settings.sslEnabled, @@ -178,34 +177,22 @@ internal constructor( override fun toString() = "FirebaseDataConnect{" + "app=${app.name}, projectId=$projectId, " + - "location=${serviceConfig.location}, serviceId=${serviceConfig.serviceId} " + - "operationSet=${serviceConfig.operationSet}, revision=${serviceConfig.revision}" + + "location=${serviceConfig.location}, " + + "serviceId=${serviceConfig.serviceId}, " + + "connector=${serviceConfig.connector}" + "}" - class ServiceConfig(serviceId: String, location: String, operationSet: String, revision: String) { - private val impl = - Impl( - serviceId = serviceId, - location = location, - operationSet = operationSet, - revision = revision - ) + class ServiceConfig(serviceId: String, location: String, connector: String) { + private val impl = Impl(serviceId = serviceId, location = location, connector = connector) val serviceId: String get() = impl.serviceId val location: String get() = impl.location - val operationSet: String - get() = impl.operationSet - val revision: String - get() = impl.revision - - private data class Impl( - val serviceId: String, - val location: String, - val operationSet: String, - val revision: String - ) + val connector: String + get() = impl.connector + + private data class Impl(val serviceId: String, val location: String, val connector: String) override fun equals(other: Any?) = (other as? ServiceConfig)?.let { other.impl == impl } ?: false @@ -213,7 +200,7 @@ internal constructor( override fun hashCode() = impl.hashCode() override fun toString() = - "ServiceConfig(serviceId=$serviceId, location=$location,operationSet=$operationSet, revision=$revision)" + "ServiceConfig(serviceId=$serviceId, location=$location, connector=$connector)" } companion object { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt index 58996692343..05644713951 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnectFactory.kt @@ -43,7 +43,7 @@ internal class FirebaseDataConnectFactory( FirebaseDataConnectInstanceKey( serviceId = serviceId, location = location, - operationSet = operationSet + connector = connector ) } @@ -54,7 +54,7 @@ internal class FirebaseDataConnectFactory( val cachedInstance = instances[key] if (cachedInstance !== null) { - throwIfIncompatible(key, cachedInstance, serviceConfig, settings) + throwIfIncompatible(key, cachedInstance, settings) return cachedInstance } @@ -120,30 +120,17 @@ internal class FirebaseDataConnectFactory( private data class FirebaseDataConnectInstanceKey( val serviceId: String, val location: String, - val operationSet: String, + val connector: String, ) { - override fun toString() = "serviceId=$serviceId, location=$location, operationSet=$operationSet" + override fun toString() = "serviceId=$serviceId, location=$location, connector=$connector" } private fun throwIfIncompatible( key: FirebaseDataConnectInstanceKey, instance: FirebaseDataConnect, - serviceConfig: FirebaseDataConnect.ServiceConfig, settings: FirebaseDataConnectSettings? ) { - val keyStr = key.run { "serviceId=$serviceId, location=$location, operationSet=$operationSet" } - - if (instance.serviceConfig.revision != serviceConfig.revision) { - throw IllegalArgumentException( - "The 'revision' of the FirebaseDataConnect instance with [$keyStr] is " + - "'${instance.serviceConfig.revision}', which is different from the " + - "'revision' of the given ServiceConfig: '${serviceConfig.revision}`; " + - "to get a FirebaseDataConnect with [$keyStr] but a different 'revision', first call " + - "close() on the existing FirebaseDataConnect instance, then call getInstance() again " + - "with the desired 'revision'." - ) - } - + val keyStr = key.run { "serviceId=$serviceId, location=$location, connector=$connector" } if (settings !== null && instance.settings != settings) { throw IllegalArgumentException( "The settings of the FirebaseDataConnect instance with [$keyStr] is " + diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt index 7594c96c9b3..36993f973a4 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/ProtoUtil.kt @@ -20,10 +20,10 @@ import com.google.protobuf.Value import com.google.protobuf.Value.KindCase import com.google.protobuf.listValueOrNull import com.google.protobuf.structValueOrNull -import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteMutationRequest -import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteMutationResponse -import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteQueryRequest -import google.firebase.dataconnect.v1main.DataServiceOuterClass.ExecuteQueryResponse +import google.firebase.dataconnect.v1main.ExecuteMutationRequest +import google.firebase.dataconnect.v1main.ExecuteMutationResponse +import google.firebase.dataconnect.v1main.ExecuteQueryRequest +import google.firebase.dataconnect.v1main.ExecuteQueryResponse import java.io.BufferedWriter import java.io.CharArrayWriter import java.io.DataOutputStream diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt index 67acc83c15f..e418548635c 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/generated/Posts.kt @@ -35,8 +35,7 @@ class PostsOperationSet( FirebaseDataConnect.ServiceConfig( serviceId = serviceId, location = location, - operationSet = "crud", - revision = "42" + connector = "crud" ), settings ) diff --git a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/connector_service.proto b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/connector_service.proto new file mode 100644 index 00000000000..7ec3b417343 --- /dev/null +++ b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/connector_service.proto @@ -0,0 +1,91 @@ +// Adapted from http://google3/google/firebase/dataconnect/v1main/connector_service.proto;rcl=596717236 + +syntax = "proto3"; + +// TODO: Change the package to "v1main" (instead of "emulator") to match production, and remove the +// "option java_package = ..." below (see https://chat.google.com/room/AAAAdvEjzno/RkgZJA4WcCE). +package google.firebase.dataconnect.emulator; + +import "google/api/field_behavior.proto"; +import "google/firebase/dataconnect/v1main/graphql_error.proto"; +import "google/protobuf/struct.proto"; + +option java_package = "google.firebase.dataconnect.v1main"; +option java_multiple_files = true; + +// Firebase Data Connect provides means to deploy a set of predefined GraphQL +// operations (queries and mutations) as a Connector. +// +// Firebase developers can build mobile and web apps that uses Connectors +// to access Data Sources directly. Connectors allow operations without +// admin credentials and help Firebase customers control the API exposure. +// +// Note: `ConnectorService` doesn't check IAM permissions and instead developers +// must define auth policies on each pre-defined operation to secure this +// connector. The auth policies typically define rules on the Firebase Auth +// token. +service ConnectorService { + // Execute a predefined query in a Connector. + rpc ExecuteQuery(ExecuteQueryRequest) returns (ExecuteQueryResponse) { + } + + // Execute a predefined mutation in a Connector. + rpc ExecuteMutation(ExecuteMutationRequest) returns (ExecuteMutationResponse) { + } +} + +// The ExecuteQuery request to Firebase Data Connect. +message ExecuteQueryRequest { + // The resource name of the connector to find the predefined query, in + // the format: + // ``` + // projects/{project}/locations/{location}/services/{service}/connectors/{connector} + // ``` + string name = 1 [(google.api.field_behavior) = REQUIRED]; + + // The name of the GraphQL operation name. + // Required because all Connector operations must be named. + // See https://graphql.org/learn/queries/#operation-name. + // (-- api-linter: core::0122::name-suffix=disabled + // aip.dev/not-precedent: Must conform to GraphQL HTTP spec standard. --) + string operation_name = 2 [(google.api.field_behavior) = REQUIRED]; + + // Values for GraphQL variables provided in this request. + google.protobuf.Struct variables = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +// The ExecuteMutation request to Firebase Data Connect. +message ExecuteMutationRequest { + // The resource name of the connector to find the predefined mutation, in + // the format: + // ``` + // projects/{project}/locations/{location}/services/{service}/connectors/{connector} + // ``` + string name = 1 [(google.api.field_behavior) = REQUIRED]; + + // The name of the GraphQL operation name. + // Required because all Connector operations must be named. + // See https://graphql.org/learn/queries/#operation-name. + // (-- api-linter: core::0122::name-suffix=disabled + // aip.dev/not-precedent: Must conform to GraphQL HTTP spec standard. --) + string operation_name = 2 [(google.api.field_behavior) = REQUIRED]; + + // Values for GraphQL variables provided in this request. + google.protobuf.Struct variables = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +// The ExecuteQuery response from Firebase Data Connect. +message ExecuteQueryResponse { + // The result of executing the requested operation. + google.protobuf.Struct data = 1; + // Errors of this response. + repeated GraphqlError errors = 2; +} + +// The ExecuteMutation response from Firebase Data Connect. +message ExecuteMutationResponse { + // The result of executing the requested operation. + google.protobuf.Struct data = 1; + // Errors of this response. + repeated GraphqlError errors = 2; +} \ No newline at end of file diff --git a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/data_service.proto b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/data_service.proto deleted file mode 100644 index 9a4a7435c96..00000000000 --- a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/data_service.proto +++ /dev/null @@ -1,101 +0,0 @@ -// Adapted from http://google3/firebase/firemat/emulator/server/api/data_service.proto;rcl=586381894 - -// API protos for the Firebase Data Connect Private API DataService. - -syntax = "proto3"; - -package google.firebase.dataconnect.v1main; - -import "google/api/annotations.proto"; -import "google/protobuf/struct.proto"; - -service DataService { - // REST API for executing a single pre-defined query. - // Use `operationSets/*/revisions/latest` to reference the most recent - // revision. - rpc ExecuteQuery(ExecuteQueryRequest) returns (ExecuteQueryResponse) { - option (google.api.http) = { - post: "/v0/{name=projects/*/locations/*/services/*/operationSets/*/revisions/*}:executeQuery" - body: "*" - }; - } - - // REST API for executing a single pre-defined mutation. - // Use `operationSets/*/revisions/latest` to reference the most recent - // revision. - rpc ExecuteMutation(ExecuteMutationRequest) - returns (ExecuteMutationResponse) { - option (google.api.http) = { - post: "/v0/{name=projects/*/locations/*/services/*/operationSets/*/revisions/*}:executeMutation" - body: "*" - }; - } -} - -message ExecuteQueryRequest { - // The resource name of the operation set revision that contains the query to - // execute, in the format: - // ``` - // projects/{project}/locations/{location}/services/{service}/operationSets/{operation_set}/revisions/{revision} - // ``` - // This is left empty when it is within StreamRequest. - string name = 1; - - // The name of the GraphQL operation. See - // https://graphql.org/learn/queries/#operation-name for more context. - string operation_name = 2; - - // Arbitrary JSON parameters (GraphQL variables) for the operation. - google.protobuf.Struct variables = 3; -} - -message ExecuteMutationRequest { - // The resource name of the operation set revision that contains the mutation - // to execute, in the format: - // ``` - // projects/{project}/locations/{location}/services/{service}/operationSets/{operation_set}/revisions/{revision} - // ``` - // This is left empty when it is within StreamRequest. - string name = 1; - - // The name of the GraphQL operation. See - // https://graphql.org/learn/queries/#operation-name for more context. - string operation_name = 2; - - // Arbitrary JSON parameters (GraphQL variables) for the operation. - google.protobuf.Struct variables = 3; -} - -message ExecuteQueryResponse { - google.protobuf.Struct data = 1; - repeated GraphqlError errors = 2; -} - -message ExecuteMutationResponse { - google.protobuf.Struct data = 1; - repeated GraphqlError errors = 2; -} - -message GraphqlError { - // Description of the error, intended for the developer. - string message = 1; - - // The location in the query where the error occurred. - repeated SourceLocation locations = 2; - - // The result field which could not be populated due to error. - google.protobuf.ListValue path = 3; - - // Additional error information in error extensions. - GraphqlErrorExtensions extensions = 6; -} - -message SourceLocation { - int32 line = 1; - int32 column = 2; -} - -// An error that occurred while executing a GraphQL query. -message GraphqlErrorExtensions { - string file = 1; -} \ No newline at end of file diff --git a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/graphql_error.proto b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/graphql_error.proto new file mode 100644 index 00000000000..1e0a4e7e800 --- /dev/null +++ b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/v1main/graphql_error.proto @@ -0,0 +1,71 @@ +// Adapted from http://google3/google/firebase/dataconnect/v1main/graphql_error.proto;rcl=597595444 + +syntax = "proto3"; + +// TODO: Change the package to "v1main" (instead of "emulator") to match production, and remove the +// "option java_package = ..." below (see https://chat.google.com/room/AAAAdvEjzno/RkgZJA4WcCE). +package google.firebase.dataconnect.emulator; + +import "google/protobuf/struct.proto"; + +option java_package = "google.firebase.dataconnect.v1main"; +option java_multiple_files = true; + +// GraphqlError conforms to the GraphQL error spec. +// https://spec.graphql.org/draft/#sec-Errors +// +// Firebase Data Connect API surfaces `GraphqlError` in various APIs: +// - Upon compile error, `UpdateSchema` and `UpdateConnector` return +// Code.Invalid_Argument with a list of `GraphqlError` in error details. +// - Upon query compile error, `ExecuteGraphql` and `ExecuteGraphqlRead` return +// Code.OK with a list of `GraphqlError` in response body. +// - Upon query execution error, `ExecuteGraphql`, `ExecuteGraphqlRead`, +// `ExecuteMutation` and `ExecuteQuery` all return Code.OK with a list of +// `GraphqlError` in response body. +message GraphqlError { + // The detailed error message. + // The message should help developer understand the underlying problem without + // leaking internal data. + string message = 1; + + // The source locations where the error occurred. + // Locations should help developers and toolings identify the source of error + // quickly. + // + // Included in admin endpoints (`ExecuteGraphql`, `ExecuteGraphqlRead`, + // `UpdateSchema` and `UpdateConnector`) to reference the provided GraphQL + // GQL document. + // + // Omitted in `ExecuteMutation` and `ExecuteQuery` since the caller shouldn't + // have access access the underlying GQL source. + repeated SourceLocation locations = 2; + + // The result field which could not be populated due to error. + // + // Clients can use path to identify whether a null result is intentional or + // caused by a runtime error. + // It should be a list of string or index from the root of GraphQL query + // document. + google.protobuf.ListValue path = 3; + + // Additional error information. + GraphqlErrorExtensions extensions = 4; +} + +// SourceLocation references a location in a GraphQL source. +message SourceLocation { + // Line number starting at 1. + int32 line = 1; + // Column number starting at 1. + int32 column = 2; +} + +// GraphqlErrorExtensions contains additional information of `GraphqlError`. +// (-- TODO(b/305311379): include more detailed error fields: +// go/firemat:api:gql-errors. --) +message GraphqlErrorExtensions { + // The source file name where the error occurred. + // Included only for `UpdateSchema` and `UpdateConnector`, it corresponds + // to `File.path` of the provided `Source`. + string file = 1; +} \ No newline at end of file From 2fc0dbea35b50e6f805ed65d2ae383e8f404e2f1 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 25 Jan 2024 12:09:32 -0500 Subject: [PATCH 131/573] demo app boilerplate added --- firebase-dataconnect/demo/README.md | 1 + firebase-dataconnect/demo/demo.gradle.kts | 66 +++++++ firebase-dataconnect/demo/proguard-rules.pro | 14 ++ .../demo/ExampleInstrumentedTest.kt | 24 +++ .../demo/src/main/AndroidManifest.xml | 28 +++ .../dataconnect/demo/FirstFragment.kt | 44 +++++ .../firebase/dataconnect/demo/MainActivity.kt | 58 ++++++ .../dataconnect/demo/SecondFragment.kt | 44 +++++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++++ .../src/main/res/layout/activity_main.xml | 33 ++++ .../demo/src/main/res/layout/content_main.xml | 19 ++ .../src/main/res/layout/fragment_first.xml | 35 ++++ .../src/main/res/layout/fragment_second.xml | 35 ++++ .../demo/src/main/res/menu/menu_main.xml | 10 ++ .../main/res/mipmap-anydpi/ic_launcher.xml | 6 + .../res/mipmap-anydpi/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../src/main/res/navigation/nav_graph.xml | 28 +++ .../demo/src/main/res/values-land/dimens.xml | 3 + .../demo/src/main/res/values-night/themes.xml | 7 + .../demo/src/main/res/values-v23/themes.xml | 9 + .../src/main/res/values-w1240dp/dimens.xml | 3 + .../src/main/res/values-w600dp/dimens.xml | 3 + .../demo/src/main/res/values/colors.xml | 5 + .../demo/src/main/res/values/dimens.xml | 3 + .../demo/src/main/res/values/strings.xml | 46 +++++ .../demo/src/main/res/values/themes.xml | 9 + .../demo/src/main/res/xml/backup_rules.xml | 13 ++ .../main/res/xml/data_extraction_rules.xml | 19 ++ .../dataconnect/demo/ExampleUnitTest.kt | 17 ++ subprojects.cfg | 1 + 41 files changed, 789 insertions(+) create mode 100644 firebase-dataconnect/demo/README.md create mode 100644 firebase-dataconnect/demo/demo.gradle.kts create mode 100644 firebase-dataconnect/demo/proguard-rules.pro create mode 100644 firebase-dataconnect/demo/src/androidTest/java/com/google/firebase/dataconnect/demo/ExampleInstrumentedTest.kt create mode 100644 firebase-dataconnect/demo/src/main/AndroidManifest.xml create mode 100644 firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/FirstFragment.kt create mode 100644 firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/MainActivity.kt create mode 100644 firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/SecondFragment.kt create mode 100644 firebase-dataconnect/demo/src/main/res/drawable/ic_launcher_background.xml create mode 100644 firebase-dataconnect/demo/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 firebase-dataconnect/demo/src/main/res/layout/activity_main.xml create mode 100644 firebase-dataconnect/demo/src/main/res/layout/content_main.xml create mode 100644 firebase-dataconnect/demo/src/main/res/layout/fragment_first.xml create mode 100644 firebase-dataconnect/demo/src/main/res/layout/fragment_second.xml create mode 100644 firebase-dataconnect/demo/src/main/res/menu/menu_main.xml create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 firebase-dataconnect/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 firebase-dataconnect/demo/src/main/res/navigation/nav_graph.xml create mode 100644 firebase-dataconnect/demo/src/main/res/values-land/dimens.xml create mode 100644 firebase-dataconnect/demo/src/main/res/values-night/themes.xml create mode 100644 firebase-dataconnect/demo/src/main/res/values-v23/themes.xml create mode 100644 firebase-dataconnect/demo/src/main/res/values-w1240dp/dimens.xml create mode 100644 firebase-dataconnect/demo/src/main/res/values-w600dp/dimens.xml create mode 100644 firebase-dataconnect/demo/src/main/res/values/colors.xml create mode 100644 firebase-dataconnect/demo/src/main/res/values/dimens.xml create mode 100644 firebase-dataconnect/demo/src/main/res/values/strings.xml create mode 100644 firebase-dataconnect/demo/src/main/res/values/themes.xml create mode 100644 firebase-dataconnect/demo/src/main/res/xml/backup_rules.xml create mode 100644 firebase-dataconnect/demo/src/main/res/xml/data_extraction_rules.xml create mode 100644 firebase-dataconnect/demo/src/test/java/com/google/firebase/dataconnect/demo/ExampleUnitTest.kt diff --git a/firebase-dataconnect/demo/README.md b/firebase-dataconnect/demo/README.md new file mode 100644 index 00000000000..8f50cbf7302 --- /dev/null +++ b/firebase-dataconnect/demo/README.md @@ -0,0 +1 @@ +A demo application that uses firebase-dataconnect. diff --git a/firebase-dataconnect/demo/demo.gradle.kts b/firebase-dataconnect/demo/demo.gradle.kts new file mode 100644 index 00000000000..755fc28ee82 --- /dev/null +++ b/firebase-dataconnect/demo/demo.gradle.kts @@ -0,0 +1,66 @@ +// Copyright 2024 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. + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.google.firebase.dataconnect.demo" + compileSdk = 33 + + defaultConfig { + minSdk = 33 + targetSdk = 33 + versionCode = 1 + versionName = "1.0.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(project(":firebase-dataconnect")) + + implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.9.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.navigation:navigation-fragment-ktx:2.6.0") + implementation("androidx.navigation:navigation-ui-ktx:2.6.0") + + testImplementation(libs.junit) + testImplementation(libs.robolectric) + testImplementation(libs.truth) + + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} diff --git a/firebase-dataconnect/demo/proguard-rules.pro b/firebase-dataconnect/demo/proguard-rules.pro new file mode 100644 index 00000000000..0ccfb18d33b --- /dev/null +++ b/firebase-dataconnect/demo/proguard-rules.pro @@ -0,0 +1,14 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Uncomment this to preserve the line number information for +# debugging stack traces. +-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +-renamesourcefileattribute SourceFile diff --git a/firebase-dataconnect/demo/src/androidTest/java/com/google/firebase/dataconnect/demo/ExampleInstrumentedTest.kt b/firebase-dataconnect/demo/src/androidTest/java/com/google/firebase/dataconnect/demo/ExampleInstrumentedTest.kt new file mode 100644 index 00000000000..6799cd01f5a --- /dev/null +++ b/firebase-dataconnect/demo/src/androidTest/java/com/google/firebase/dataconnect/demo/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.google.firebase.dataconnect.demo + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.google.firebase.dataconnect.demo", appContext.packageName) + } +} \ No newline at end of file diff --git a/firebase-dataconnect/demo/src/main/AndroidManifest.xml b/firebase-dataconnect/demo/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..27bbcff3fb9 --- /dev/null +++ b/firebase-dataconnect/demo/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/FirstFragment.kt b/firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/FirstFragment.kt new file mode 100644 index 00000000000..af062c39e90 --- /dev/null +++ b/firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/FirstFragment.kt @@ -0,0 +1,44 @@ +package com.google.firebase.dataconnect.demo + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.google.firebase.dataconnect.demo.databinding.FragmentFirstBinding + +/** + * A simple [Fragment] subclass as the default destination in the navigation. + */ +class FirstFragment : Fragment() { + + private var _binding: FragmentFirstBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + _binding = FragmentFirstBinding.inflate(inflater, container, false) + return binding.root + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonFirst.setOnClickListener { + findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/MainActivity.kt b/firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/MainActivity.kt new file mode 100644 index 00000000000..c5777cb2dd7 --- /dev/null +++ b/firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/MainActivity.kt @@ -0,0 +1,58 @@ +package com.google.firebase.dataconnect.demo + +import android.os.Bundle +import com.google.android.material.snackbar.Snackbar +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.setupActionBarWithNavController +import android.view.Menu +import android.view.MenuItem +import com.google.firebase.dataconnect.demo.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + + val navController = findNavController(R.id.nav_host_fragment_content_main) + appBarConfiguration = AppBarConfiguration(navController.graph) + setupActionBarWithNavController(navController, appBarConfiguration) + + binding.fab.setOnClickListener { view -> + Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) + .setAction("Action", null).show() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + return when (item.itemId) { + R.id.action_settings -> true + else -> super.onOptionsItemSelected(item) + } + } + + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment_content_main) + return navController.navigateUp(appBarConfiguration) + || super.onSupportNavigateUp() + } +} \ No newline at end of file diff --git a/firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/SecondFragment.kt b/firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/SecondFragment.kt new file mode 100644 index 00000000000..93ff535f565 --- /dev/null +++ b/firebase-dataconnect/demo/src/main/java/com/google/firebase/dataconnect/demo/SecondFragment.kt @@ -0,0 +1,44 @@ +package com.google.firebase.dataconnect.demo + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.google.firebase.dataconnect.demo.databinding.FragmentSecondBinding + +/** + * A simple [Fragment] subclass as the second destination in the navigation. + */ +class SecondFragment : Fragment() { + + private var _binding: FragmentSecondBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + _binding = FragmentSecondBinding.inflate(inflater, container, false) + return binding.root + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonSecond.setOnClickListener { + findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/firebase-dataconnect/demo/src/main/res/drawable/ic_launcher_background.xml b/firebase-dataconnect/demo/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000000..07d5da9cbf1 --- /dev/null +++ b/firebase-dataconnect/demo/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase-dataconnect/demo/src/main/res/drawable/ic_launcher_foreground.xml b/firebase-dataconnect/demo/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000000..2b068d11462 --- /dev/null +++ b/firebase-dataconnect/demo/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/firebase-dataconnect/demo/src/main/res/layout/activity_main.xml b/firebase-dataconnect/demo/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000000..16313e04ec9 --- /dev/null +++ b/firebase-dataconnect/demo/src/main/res/layout/activity_main.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/firebase-dataconnect/demo/src/main/res/layout/content_main.xml b/firebase-dataconnect/demo/src/main/res/layout/content_main.xml new file mode 100644 index 00000000000..e416e1c18d5 --- /dev/null +++ b/firebase-dataconnect/demo/src/main/res/layout/content_main.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/firebase-dataconnect/demo/src/main/res/layout/fragment_first.xml b/firebase-dataconnect/demo/src/main/res/layout/fragment_first.xml new file mode 100644 index 00000000000..44baecd7d4a --- /dev/null +++ b/firebase-dataconnect/demo/src/main/res/layout/fragment_first.xml @@ -0,0 +1,35 @@ + + + + + +