diff --git a/openfeature-provider/android/.gitignore b/openfeature-provider/android/.gitignore new file mode 100644 index 00000000..3abdecab --- /dev/null +++ b/openfeature-provider/android/.gitignore @@ -0,0 +1,33 @@ +# Gradle +.gradle/ +build/ +**/build/ + +# IDE +.idea/ +*.iml +local.properties + +# OS +.DS_Store +Thumbs.db + +# Kotlin +*.class +*.log +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# Android +*.apk +*.ap_ +*.aab +*.dex +/captures +.externalNativeBuild +.cxx diff --git a/openfeature-provider/android/Makefile b/openfeature-provider/android/Makefile new file mode 100644 index 00000000..b1039a17 --- /dev/null +++ b/openfeature-provider/android/Makefile @@ -0,0 +1,22 @@ +.PHONY: build test clean + +build: + ./gradlew build + +test: + ./gradlew test + +clean: + ./gradlew clean + +assemble: + ./gradlew assemble + +lint: + ./gradlew lint + +publish-local: + ./gradlew publishToMavenLocal + +copy-wasm: + cp ../../wasm/confidence_resolver.wasm confidence-provider/src/main/resources/wasm/ diff --git a/openfeature-provider/android/README.md b/openfeature-provider/android/README.md new file mode 100644 index 00000000..4ae26d6c --- /dev/null +++ b/openfeature-provider/android/README.md @@ -0,0 +1,161 @@ +# Confidence Android OpenFeature Provider + +Local-resolution OpenFeature provider for Android that evaluates Confidence feature flags using a WebAssembly (WASM) resolver. + +## Features + +- **Local Resolution**: Evaluates flags locally using an embedded WASM resolver for fast, low-latency flag evaluation +- **Automatic State Sync**: Periodically syncs flag configurations from the Confidence CDN +- **Static Context Support**: Designed for Android's static context pattern where evaluation context is set once and reused +- **Sticky Assignments**: Supports maintaining consistent variant assignments via materialization stores +- **Background Processing**: Uses Kotlin coroutines for non-blocking state polling and log flushing + +## Requirements + +- Android API 26+ (Android 8.0 Oreo) +- Kotlin 1.9+ + +## Installation + +Add the dependency to your `build.gradle.kts`: + +```kotlin +dependencies { + implementation("com.spotify.confidence:confidence-android-provider:0.1.0") +} +``` + +## Usage + +### Basic Setup + +```kotlin +import com.spotify.confidence.android.ConfidenceLocalProvider +import dev.openfeature.sdk.OpenFeatureAPI +import dev.openfeature.sdk.ImmutableContext +import dev.openfeature.sdk.Value + +// Create the provider +val provider = ConfidenceLocalProvider("your-client-secret") + +// Set global evaluation context (Android static context pattern) +OpenFeatureAPI.setEvaluationContext( + ImmutableContext( + targetingKey = "user-123", + attributes = mapOf( + "country" to Value.String("US"), + "premium" to Value.Boolean(true) + ) + ) +) + +// Initialize the provider +OpenFeatureAPI.setProviderAndWait(provider) + +// Get a client and resolve flags +val client = OpenFeatureAPI.getClient() +val showFeature = client.getBooleanValue("show-new-feature", false) +val buttonColor = client.getStringValue("button-color", "blue") +``` + +### Advanced Configuration + +```kotlin +import com.spotify.confidence.android.LocalProviderConfig +import java.time.Duration + +val config = LocalProviderConfig.Builder() + .statePollInterval(Duration.ofSeconds(60)) // How often to check for state updates + .logFlushInterval(Duration.ofSeconds(30)) // How often to flush flag logs + .useRemoteMaterializationStore(true) // Enable remote sticky assignments + .build() + +val provider = ConfidenceLocalProvider(config, "your-client-secret") +``` + +### Nested Flag Values + +Access nested values using dot notation: + +```kotlin +// If your flag has a structure like: { "button": { "color": "red", "size": 12 } } +val buttonColor = client.getStringValue("my-flag.button.color", "blue") +val buttonSize = client.getIntegerValue("my-flag.button.size", 10) +``` + +### Shutdown + +Clean up resources when the provider is no longer needed: + +```kotlin +provider.shutdown() +``` + +## Static Context Pattern + +Android's OpenFeature SDK uses a static context pattern, meaning: + +1. The evaluation context is set once during app initialization +2. This context is reused for all subsequent flag evaluations +3. Context updates are applied to all future evaluations + +This differs from server-side SDKs where context is passed per-request. The Confidence provider fully supports this pattern: + +```kotlin +// Set context once at startup +OpenFeatureAPI.setEvaluationContext( + ImmutableContext( + targetingKey = userId, + attributes = userAttributes + ) +) + +// All evaluations use this context +val feature1 = client.getBooleanValue("feature-1", false) +val feature2 = client.getStringValue("feature-2", "default") + +// Update context when user info changes +OpenFeatureAPI.setEvaluationContext( + ImmutableContext( + targetingKey = newUserId, + attributes = newUserAttributes + ) +) +``` + +## Architecture + +The provider consists of these components: + +- **ConfidenceLocalProvider**: Main OpenFeature provider implementation +- **WasmResolver**: Interfaces with the Confidence WASM resolver binary via Chicory +- **SwapWasmResolverApi**: Manages WASM instance lifecycle with hot-swapping support +- **StateFetcher**: Fetches and caches resolver state from CDN with ETag support +- **GrpcWasmFlagLogger**: Sends flag resolution logs via gRPC +- **MaterializationStore**: Interface for sticky assignment storage +- **ConfidenceValue**: Type-safe value system compatible with Confidence SDK +- **ValueConversions**: Utilities for converting between OpenFeature and Confidence types +- **TypeMapper**: Converts between Protobuf and OpenFeature types + +## Compatibility with Existing Confidence SDK + +This provider is designed to be compatible with the [Confidence SDK Android](https://github.com/spotify/confidence-sdk-android). +It uses the same: +- Value type system (ConfidenceValue) +- Context handling patterns +- Initialization strategies + +The main difference is that this provider uses local WASM-based resolution instead of remote HTTP/gRPC calls. + +## Configuration Options + +| Option | Description | Default | +|--------|-------------|---------| +| `statePollInterval` | How often to poll for state updates | 30 seconds | +| `logFlushInterval` | How often to flush flag logs | 10 seconds | +| `initialisationStrategy` | How to initialize (FetchAndActivate or ActivateAndFetchAsync) | FetchAndActivate | +| `useRemoteMaterializationStore` | Whether to use remote gRPC for sticky assignments | false | + +## License + +Apache 2.0 diff --git a/openfeature-provider/android/build.gradle.kts b/openfeature-provider/android/build.gradle.kts new file mode 100644 index 00000000..41f1b97c --- /dev/null +++ b/openfeature-provider/android/build.gradle.kts @@ -0,0 +1,7 @@ +// Root build file for Confidence Android OpenFeature Provider +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.protobuf) apply false +} diff --git a/openfeature-provider/android/confidence-provider/build.gradle.kts b/openfeature-provider/android/confidence-provider/build.gradle.kts new file mode 100644 index 00000000..c39889b5 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/build.gradle.kts @@ -0,0 +1,175 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.protobuf) +} + +// Configuration for Chicory build-time WASM compilation +val chicoryCompiler: Configuration by configurations.creating { + isTransitive = false +} + +// Task to compile WASM to JVM bytecode at build time +val compileWasm by tasks.registering { + group = "build" + description = "Compiles WASM to JVM bytecode using Chicory build-time compiler" + + val wasmFile = file("src/main/resources/wasm/confidence_resolver.wasm") + val outputSourceDir = layout.buildDirectory.dir("generated/wasm-sources/java") + val outputClassDir = layout.buildDirectory.dir("generated/wasm-classes") + val outputMetaDir = layout.buildDirectory.dir("generated/wasm-meta") + + inputs.file(wasmFile) + inputs.files(chicoryCompiler) + outputs.dir(outputSourceDir) + outputs.dir(outputClassDir) + outputs.dir(outputMetaDir) + + doLast { + outputSourceDir.get().asFile.mkdirs() + outputClassDir.get().asFile.mkdirs() + outputMetaDir.get().asFile.mkdirs() + + val cliJar = chicoryCompiler.singleFile + exec { + commandLine( + "java", "-jar", + cliJar.absolutePath, + "--source-dir=${outputSourceDir.get().asFile.absolutePath}", + "--class-dir=${outputClassDir.get().asFile.absolutePath}", + "--wasm-dir=${outputMetaDir.get().asFile.absolutePath}", + "--prefix=com.spotify.confidence.wasm.ConfidenceResolver", + "--interpreter-fallback=WARN", + wasmFile.absolutePath + ) + } + } +} + +android { + namespace = "com.spotify.confidence.android" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + packaging { + resources { + // Exclude proto files that conflict with protobuf-javalite + excludes += "google/protobuf/*.proto" + excludes += "google/api/*.proto" + excludes += "google/type/*.proto" + } + } + + sourceSets { + getByName("main") { + resources.srcDirs("src/main/resources") + // Include pre-compiled WASM meta file + resources.srcDirs(layout.buildDirectory.dir("generated/wasm-meta")) + // Include pre-compiled WASM Java source + java.srcDirs(layout.buildDirectory.dir("generated/wasm-sources/java")) + } + } +} + +// Wire WASM compilation into the build +tasks.named("preBuild") { + dependsOn("compileWasm") +} + +// Add pre-compiled WASM classes to the compile classpath and bundle +android.libraryVariants.all { + val variant = this + val variantName = variant.name.replaceFirstChar { it.uppercaseChar() } + + // Add to compile classpath + val compileTask = tasks.named("compile${variantName}JavaWithJavac") + compileTask.configure { + dependsOn("compileWasm") + classpath += files(layout.buildDirectory.dir("generated/wasm-classes")) + } + + // Register the pre-compiled classes as additional class files + variant.registerPostJavacGeneratedBytecode(files(layout.buildDirectory.dir("generated/wasm-classes"))) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}" + } + plugins { + create("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:${libs.versions.grpc.get()}" + } + } + generateProtoTasks { + all().forEach { task -> + task.builtins { + create("java") { + option("lite") + } + } + task.plugins { + create("grpc") { + option("lite") + } + } + } + } +} + + +dependencies { + // Chicory build-time WASM compiler (CLI tool) + chicoryCompiler("com.dylibso.chicory:build-time-compiler-cli-experimental:${libs.versions.chicory.get()}") + + // OpenFeature SDK for Android + implementation(libs.openfeature.android) + + // WASM runtime - Chicory + implementation(libs.bundles.chicory) + + // Protobuf + implementation(libs.protobuf.javalite) + + // gRPC for Android + implementation(libs.bundles.grpc) + + // javax.annotation for gRPC generated code + compileOnly("javax.annotation:javax.annotation-api:1.3.2") + + // Networking + implementation(libs.okhttp) + + // Coroutines + implementation(libs.bundles.coroutines) + + // Testing + testImplementation(libs.junit) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockk) +} diff --git a/openfeature-provider/android/confidence-provider/consumer-rules.pro b/openfeature-provider/android/confidence-provider/consumer-rules.pro new file mode 100644 index 00000000..8a5f9a84 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/consumer-rules.pro @@ -0,0 +1,8 @@ +# Consumer ProGuard Rules for Confidence Android Provider +# These rules are automatically included in consumer apps + +# Keep protobuf generated classes +-keep class * extends com.google.protobuf.GeneratedMessageLite { *; } + +# Keep gRPC service stubs +-keepclassmembers class * extends io.grpc.stub.AbstractStub { *; } diff --git a/openfeature-provider/android/confidence-provider/proguard-rules.pro b/openfeature-provider/android/confidence-provider/proguard-rules.pro new file mode 100644 index 00000000..742af77e --- /dev/null +++ b/openfeature-provider/android/confidence-provider/proguard-rules.pro @@ -0,0 +1,18 @@ +# Confidence Android Provider ProGuard Rules + +# Keep protobuf classes +-keep class com.google.protobuf.** { *; } +-keep class * extends com.google.protobuf.GeneratedMessageLite { *; } + +# Keep gRPC classes +-keep class io.grpc.** { *; } +-keepnames class io.grpc.** { *; } + +# Keep Chicory WASM runtime +-keep class com.dylibso.chicory.** { *; } + +# Keep OpenFeature classes +-keep class dev.openfeature.** { *; } + +# Keep our SDK classes +-keep class com.spotify.confidence.android.** { *; } diff --git a/openfeature-provider/android/confidence-provider/src/main/AndroidManifest.xml b/openfeature-provider/android/confidence-provider/src/main/AndroidManifest.xml new file mode 100644 index 00000000..19d2638e --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ConfidenceLocalProvider.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ConfidenceLocalProvider.kt new file mode 100644 index 00000000..7979fa91 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ConfidenceLocalProvider.kt @@ -0,0 +1,431 @@ +package com.spotify.confidence.android + +import android.util.Log +import com.google.protobuf.Struct +import com.spotify.confidence.flags.resolver.v1.ResolveFlagsRequest +import com.spotify.confidence.flags.resolver.v1.ResolveWithStickyRequest +import com.spotify.confidence.flags.resolver.v1.Sdk +import com.spotify.confidence.flags.resolver.v1.SdkId +import dev.openfeature.kotlin.sdk.EvaluationContext +import dev.openfeature.kotlin.sdk.FeatureProvider +import dev.openfeature.kotlin.sdk.Hook +import dev.openfeature.kotlin.sdk.ProviderEvaluation +import dev.openfeature.kotlin.sdk.ProviderMetadata +import dev.openfeature.kotlin.sdk.Value +import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicReference + +/** + * OpenFeature provider for Confidence feature flags using local resolution. + * + * This provider evaluates feature flags locally using a WebAssembly (WASM) resolver. It + * periodically syncs flag configurations from the Confidence service and caches them locally for + * fast, low-latency flag evaluation. + * + * **Android Static Context Pattern:** + * Unlike server-side providers where context is passed per-request, Android uses a static + * context pattern. The evaluation context is typically set once during app initialization + * via `OpenFeatureAPI.setEvaluationContext()` and used for all subsequent flag evaluations. + * This provider fully supports this pattern while still allowing per-evaluation context + * overrides when needed. + * + * **Usage Example:** + * ```kotlin + * val clientSecret = "your-application-client-secret" + * val config = LocalProviderConfig() + * val provider = ConfidenceLocalProvider(config, clientSecret) + * + * // Set global evaluation context (static context pattern) + * OpenFeatureAPI.setEvaluationContext( + * ImmutableContext( + * targetingKey = "user-123", + * attributes = mapOf("country" to Value.String("US")) + * ) + * ) + * + * OpenFeatureAPI.setProviderAndWait(provider) + * + * val client = OpenFeatureAPI.getClient() + * val flagValue = client.getStringValue("my-flag", "default-value") + * ``` + */ +class ConfidenceLocalProvider( + private val config: LocalProviderConfig = LocalProviderConfig(), + private val clientSecret: String, + private val materializationStore: MaterializationStore = UnsupportedMaterializationStore() +) : FeatureProvider { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var statePollJob: Job? = null + private var logFlushJob: Job? = null + + private val stateFetcher = StateFetcher(clientSecret, config.httpClient ?: okhttp3.OkHttpClient()) + private val flagLogger: WasmFlagLogger = GrpcWasmFlagLogger(clientSecret, config.channelFactory) + private lateinit var wasmResolveApi: ResolverApi + + private val resolverState = AtomicReference(ByteArray(0)) + private val accountIdRef = AtomicReference("") + + @Volatile + private var initialized = false + + companion object { + private const val TAG = "ConfidenceLocalProvider" + internal const val PROVIDER_ID = "SDK_ID_KOTLIN_CONFIDENCE_LOCAL" + + /** + * Creates a new ConfidenceLocalProvider with the given configuration. + * + * @param clientSecret The Confidence client secret + * @param config Configuration options for the provider + * @param materializationStore Optional custom materialization store for sticky assignments + * @return A configured ConfidenceLocalProvider instance + */ + @JvmStatic + @JvmOverloads + fun create( + clientSecret: String, + config: LocalProviderConfig = LocalProviderConfig(), + materializationStore: MaterializationStore = UnsupportedMaterializationStore() + ): ConfidenceLocalProvider { + return ConfidenceLocalProvider( + config = config, + clientSecret = clientSecret, + materializationStore = materializationStore + ) + } + } + + /** + * Secondary constructor for simple initialization without custom materialization store. + */ + constructor(clientSecret: String) : this(LocalProviderConfig(), clientSecret) + + /** + * Secondary constructor with config and clientSecret. + */ + constructor(config: LocalProviderConfig, clientSecret: String) : this( + config, + clientSecret, + UnsupportedMaterializationStore() + ) + + override val metadata: ProviderMetadata = object : ProviderMetadata { + override val name: String = "confidence-sdk-android-local" + } + + override val hooks: List> = emptyList() + + override suspend fun initialize(initialContext: EvaluationContext?) { + try { + // Fetch initial state + stateFetcher.reload() + resolverState.set(stateFetcher.provide()) + accountIdRef.set(stateFetcher.accountId()) + + // Only initialize WASM if we got valid state + if (accountIdRef.get().isNotEmpty()) { + wasmResolveApi = SwapWasmResolverApi( + flagLogger, + resolverState.get(), + accountIdRef.get(), + materializationStore + ) + initialized = true + } else { + Log.w(TAG, "Initial state load failed, provider starting in NOT_READY state, serving default values.") + } + + // Start background tasks + startBackgroundTasks() + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize provider", e) + } + } + + override fun shutdown() { + statePollJob?.cancel() + logFlushJob?.cancel() + + if (initialized) { + wasmResolveApi.close() + } + } + + /** + * Called when the evaluation context is updated. + * This triggers a state refresh to ensure flags are evaluated with the new context. + */ + override suspend fun onContextSet( + oldContext: EvaluationContext?, + newContext: EvaluationContext + ) { + // Context has changed - refresh state to get updated resolutions + Log.d(TAG, "Context updated, refreshing state...") + try { + stateFetcher.reload() + resolverState.set(stateFetcher.provide()) + accountIdRef.set(stateFetcher.accountId()) + + if (initialized && ::wasmResolveApi.isInitialized) { + wasmResolveApi.updateStateAndFlushLogs( + resolverState.get(), + accountIdRef.get() + ) + } + } catch (e: Exception) { + Log.w(TAG, "Failed to refresh state after context change", e) + } + } + + override fun getBooleanEvaluation( + key: String, + defaultValue: Boolean, + context: EvaluationContext? + ): ProviderEvaluation { + return getCastedEvaluation(key, Value.Boolean(defaultValue), context) { + (it as? Value.Boolean)?.boolean + } + } + + override fun getStringEvaluation( + key: String, + defaultValue: String, + context: EvaluationContext? + ): ProviderEvaluation { + return getCastedEvaluation(key, Value.String(defaultValue), context) { + (it as? Value.String)?.string + } + } + + override fun getIntegerEvaluation( + key: String, + defaultValue: Int, + context: EvaluationContext? + ): ProviderEvaluation { + return getCastedEvaluation(key, Value.Integer(defaultValue), context) { + (it as? Value.Integer)?.integer + } + } + + override fun getDoubleEvaluation( + key: String, + defaultValue: Double, + context: EvaluationContext? + ): ProviderEvaluation { + return getCastedEvaluation(key, Value.Double(defaultValue), context) { + (it as? Value.Double)?.double + } + } + + override fun getObjectEvaluation( + key: String, + defaultValue: Value, + context: EvaluationContext? + ): ProviderEvaluation { + if (!initialized) { + return ProviderEvaluation( + value = defaultValue, + reason = "Provider not initialized" + ) + } + + val flagPath = try { + FlagPath.parse(key) + } catch (e: IllegalArgumentException) { + Log.w(TAG, e.message ?: "Invalid flag path") + throw OpenFeatureError.GeneralError(e.message ?: "Invalid flag path") + } + + val evaluationContext = TypeMapper.evaluationContextToStruct(context) + + try { + val requestFlagName = "flags/${flagPath.flag}" + + val req = ResolveFlagsRequest.newBuilder() + .addFlags(requestFlagName) + .setApply(true) + .setClientSecret(clientSecret) + .setEvaluationContext( + Struct.newBuilder().putAllFields(evaluationContext.fieldsMap).build() + ) + .setSdk( + Sdk.newBuilder() + .setId(SdkId.SDK_ID_KOTLIN_PROVIDER) + .setVersion(Version.VERSION) + .build() + ) + .build() + + val resolveFlagResponse = wasmResolveApi + .resolveWithSticky( + ResolveWithStickyRequest.newBuilder() + .setResolveRequest(req) + .setFailFastOnSticky(false) + .build() + ) + .get() + + if (resolveFlagResponse.resolvedFlagsList.isEmpty()) { + Log.w(TAG, "No active flag '${flagPath.flag}' was found") + throw OpenFeatureError.FlagNotFoundError("No active flag '${flagPath.flag}' was found") + } + + val responseFlagName = resolveFlagResponse.getResolvedFlags(0).flag + if (requestFlagName != responseFlagName) { + val unexpectedFlag = responseFlagName.removePrefix("flags/") + Log.w(TAG, "Unexpected flag '$unexpectedFlag' from remote") + throw OpenFeatureError.FlagNotFoundError("Unexpected flag '$unexpectedFlag' from remote") + } + + val resolvedFlag = resolveFlagResponse.getResolvedFlags(0) + + return if (resolvedFlag.variant.isEmpty()) { + ProviderEvaluation( + value = defaultValue, + reason = "The server returned no assignment for the flag. Typically, this happens " + + "if no configured rules matches the given evaluation context." + ) + } else { + val fullValue = TypeMapper.fromProto(resolvedFlag.value, resolvedFlag.flagSchema) + + // If a path is given, extract expected portion from the structured value + var value = getValueForPath(flagPath.path, fullValue) + + if (value is Value.Null) { + value = defaultValue + } + + ProviderEvaluation( + value = value, + reason = resolvedFlag.reason.toString(), + variant = resolvedFlag.variant + ) + } + } catch (e: OpenFeatureError) { + throw e + } catch (e: Exception) { + Log.e(TAG, "Error resolving flag", e) + throw OpenFeatureError.GeneralError("Error resolving flag: ${e.message}") + } + } + + private fun getCastedEvaluation( + key: String, + wrappedDefaultValue: Value, + context: EvaluationContext?, + cast: (Value) -> T? + ): ProviderEvaluation { + val objectEvaluation = getObjectEvaluation(key, wrappedDefaultValue, context) + + val castedValue = cast(objectEvaluation.value) + ?: run { + Log.w(TAG, "Cannot cast value '${objectEvaluation.value}' to expected type") + throw OpenFeatureError.TypeMismatchError("Cannot cast value '${objectEvaluation.value}' to expected type") + } + + @Suppress("UNCHECKED_CAST") + return ProviderEvaluation( + value = castedValue, + variant = objectEvaluation.variant, + reason = objectEvaluation.reason + ) + } + + private fun startBackgroundTasks() { + // State refresh task + statePollJob = scope.launch { + while (isActive) { + delay(config.statePollInterval.toMillis()) + try { + stateFetcher.reload() + resolverState.set(stateFetcher.provide()) + accountIdRef.set(stateFetcher.accountId()) + + if (accountIdRef.get().isNotEmpty()) { + if (!initialized) { + wasmResolveApi = SwapWasmResolverApi( + flagLogger, + resolverState.get(), + accountIdRef.get(), + materializationStore + ) + initialized = true + Log.i(TAG, "Provider recovered and is now READY") + } else if (::wasmResolveApi.isInitialized) { + wasmResolveApi.updateStateAndFlushLogs( + resolverState.get(), + accountIdRef.get() + ) + } + } + } catch (e: Exception) { + Log.w(TAG, "Failed to refresh state", e) + } + } + } + + // Log flush task + logFlushJob = scope.launch { + while (isActive) { + delay(config.logFlushInterval.toMillis()) + try { + if (initialized && ::wasmResolveApi.isInitialized) { + wasmResolveApi.updateStateAndFlushLogs( + resolverState.get(), + accountIdRef.get() + ) + } + } catch (e: Exception) { + Log.w(TAG, "Failed to flush logs", e) + } + } + } + } + + private fun getValueForPath(path: List, value: Value): Value { + if (path.isEmpty()) { + return value + } + + var current = value + for (segment in path) { + val structure = (current as? Value.Structure)?.structure ?: return Value.Null + current = structure[segment] ?: return Value.Null + } + return current + } +} + +/** + * Represents a parsed flag path (e.g., "my-flag.nested.value"). + */ +internal data class FlagPath( + val flag: String, + val path: List +) { + companion object { + fun parse(key: String): FlagPath { + if (key.isBlank()) { + throw IllegalArgumentException("Flag key cannot be empty") + } + + val parts = key.split(".") + if (parts.isEmpty()) { + throw IllegalArgumentException("Invalid flag key: $key") + } + + return FlagPath( + flag = parts[0], + path = if (parts.size > 1) parts.subList(1, parts.size) else emptyList() + ) + } + } +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ConfidenceValue.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ConfidenceValue.kt new file mode 100644 index 00000000..192646eb --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ConfidenceValue.kt @@ -0,0 +1,62 @@ +package com.spotify.confidence.android + +import java.util.Date + +/** + * Sealed interface representing all possible Confidence value types. + * Mirrors the existing Confidence SDK's value system for compatibility. + */ +sealed interface ConfidenceValue { + data class String(val string: kotlin.String) : ConfidenceValue { + override fun toString() = string + } + + data class Double(val double: kotlin.Double) : ConfidenceValue { + override fun toString() = double.toString() + } + + data class Boolean(val boolean: kotlin.Boolean) : ConfidenceValue { + override fun toString() = boolean.toString() + } + + data class Integer(val integer: Int) : ConfidenceValue { + override fun toString() = integer.toString() + } + + data class Struct(val map: Map) : ConfidenceValue { + override fun toString() = map.toString() + + fun getValue(key: kotlin.String): ConfidenceValue? = map[key] + } + + data class List(val list: kotlin.collections.List) : ConfidenceValue { + override fun toString() = list.toString() + } + + data class Timestamp(val dateTime: Date) : ConfidenceValue { + override fun toString() = dateTime.toString() + } + + object Null : ConfidenceValue { + override fun toString() = "null" + } + + companion object { + fun stringList(list: kotlin.collections.List) = + List(list.map(ConfidenceValue::String)) + + fun doubleList(list: kotlin.collections.List) = + List(list.map(ConfidenceValue::Double)) + + fun booleanList(list: kotlin.collections.List) = + List(list.map(ConfidenceValue::Boolean)) + + fun integerList(list: kotlin.collections.List) = + List(list.map(ConfidenceValue::Integer)) + } +} + +/** + * Type alias for context maps. + */ +typealias ConfidenceValueMap = Map diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/Evaluation.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/Evaluation.kt new file mode 100644 index 00000000..e59b059e --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/Evaluation.kt @@ -0,0 +1,64 @@ +package com.spotify.confidence.android + +/** + * Represents the result of a flag evaluation. + */ +data class Evaluation( + val value: T, + val variant: String? = null, + val reason: ResolveReason, + val errorCode: ErrorCode? = null, + val errorMessage: String? = null +) + +/** + * Enum representing the reason for a flag resolution result. + */ +enum class ResolveReason { + /** Unspecified enum. */ + RESOLVE_REASON_UNSPECIFIED, + + /** The flag was successfully resolved because one rule matched. */ + RESOLVE_REASON_MATCH, + + /** The flag value is from cache and may be stale. */ + RESOLVE_REASON_STALE, + + /** The flag could not be resolved because no rule matched. */ + RESOLVE_REASON_NO_SEGMENT_MATCH, + + /** The flag could not be resolved because the matching rule had no variant. */ + RESOLVE_REASON_NO_TREATMENT_MATCH, + + /** The flag could not be resolved because the targeting key is invalid. */ + RESOLVE_REASON_TARGETING_KEY_ERROR, + + /** The flag could not be resolved because it was archived. */ + RESOLVE_REASON_FLAG_ARCHIVED, + + /** Default fallback reason. */ + DEFAULT, + + /** An error occurred during evaluation. */ + ERROR +} + +/** + * Error codes for flag evaluation errors. + */ +enum class ErrorCode { + /** The provider is not ready yet. */ + PROVIDER_NOT_READY, + + /** The requested flag was not found. */ + FLAG_NOT_FOUND, + + /** Error parsing the flag value. */ + PARSE_ERROR, + + /** The evaluation context is invalid. */ + INVALID_CONTEXT, + + /** General error. */ + GENERAL +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/GrpcWasmFlagLogger.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/GrpcWasmFlagLogger.kt new file mode 100644 index 00000000..0af0479e --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/GrpcWasmFlagLogger.kt @@ -0,0 +1,138 @@ +package com.spotify.confidence.android + +import android.util.Log +import com.spotify.confidence.flags.resolver.v1.InternalFlagLoggerServiceGrpc +import com.spotify.confidence.flags.resolver.v1.WriteFlagLogsRequest +import io.grpc.ManagedChannel +import io.grpc.Metadata +import io.grpc.stub.MetadataUtils +import java.time.Duration +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +/** + * Flag logger that sends flag resolution events to the Confidence service via gRPC. + */ +internal class GrpcWasmFlagLogger( + private val clientSecret: String, + private val channelFactory: ChannelFactory = DefaultChannelFactory(), + private val shutdownTimeout: Duration = DEFAULT_SHUTDOWN_TIMEOUT +) : WasmFlagLogger { + + private val executorService: ExecutorService = Executors.newCachedThreadPool() + private var channel: ManagedChannel? = null + + companion object { + private const val TAG = "GrpcWasmFlagLogger" + private val DEFAULT_SHUTDOWN_TIMEOUT = Duration.ofSeconds(10) + private const val AUTH_HEADER = "authorization" + } + + private fun getOrCreateChannel(): ManagedChannel { + return channel ?: channelFactory.create().also { channel = it } + } + + private fun createStub(): InternalFlagLoggerServiceGrpc.InternalFlagLoggerServiceBlockingStub { + val metadata = Metadata() + metadata.put( + Metadata.Key.of(AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER), + "ClientSecret $clientSecret" + ) + + return InternalFlagLoggerServiceGrpc.newBlockingStub(getOrCreateChannel()) + .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)) + .withDeadlineAfter(30, TimeUnit.SECONDS) + } + + override fun write(logData: ByteArray) { + if (logData.isEmpty()) { + Log.d(TAG, "Skipping empty flag log") + return + } + + executorService.submit { + try { + val request = WriteFlagLogsRequest.parseFrom(logData) + Log.d(TAG, "Sending ${logData.size} bytes of log data (${request.flagAssignedCount} assignments)") + + val stub = createStub() + stub.clientWriteFlagLogs(request) + + Log.d(TAG, "Successfully sent flag logs") + } catch (e: Exception) { + Log.e(TAG, "Failed to write flag logs", e) + } + } + } + + override fun writeSync(logData: ByteArray) { + if (logData.isEmpty()) { + Log.d(TAG, "Skipping empty flag log") + return + } + + try { + val request = WriteFlagLogsRequest.parseFrom(logData) + Log.d(TAG, "Synchronously sending ${logData.size} bytes of log data") + + val stub = createStub() + stub.clientWriteFlagLogs(request) + + Log.d(TAG, "Successfully sent flag logs synchronously") + } catch (e: Exception) { + Log.e(TAG, "Failed to write flag logs synchronously", e) + } + } + + override fun shutdown() { + executorService.shutdown() + try { + if (!executorService.awaitTermination(shutdownTimeout.toMillis(), TimeUnit.MILLISECONDS)) { + Log.w(TAG, "Flag logger executor did not terminate within ${shutdownTimeout.seconds} seconds") + executorService.shutdownNow() + } else { + Log.d(TAG, "Flag logger executor terminated gracefully") + } + } catch (e: InterruptedException) { + Log.w(TAG, "Interrupted while waiting for flag logger shutdown", e) + executorService.shutdownNow() + Thread.currentThread().interrupt() + } + + channel?.let { ch -> + try { + ch.shutdown() + if (!ch.awaitTermination(5, TimeUnit.SECONDS)) { + ch.shutdownNow() + } + } catch (e: Exception) { + Log.w(TAG, "Error shutting down gRPC channel", e) + } + } + } +} + +/** + * Factory for creating gRPC channels. + */ +interface ChannelFactory { + fun create(): ManagedChannel +} + +/** + * Default channel factory that creates a channel to the Confidence edge service. + */ +class DefaultChannelFactory : ChannelFactory { + companion object { + private const val CONFIDENCE_HOST = "edge-grpc.spotify.com" + private const val CONFIDENCE_PORT = 443 + } + + override fun create(): ManagedChannel { + return io.grpc.okhttp.OkHttpChannelBuilder + .forAddress(CONFIDENCE_HOST, CONFIDENCE_PORT) + .useTransportSecurity() + .build() + } +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/InitialisationStrategy.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/InitialisationStrategy.kt new file mode 100644 index 00000000..869ef3c9 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/InitialisationStrategy.kt @@ -0,0 +1,24 @@ +package com.spotify.confidence.android + +/** + * Strategy for provider initialization. + * + * Determines how the provider behaves during initialization: + * - [FetchAndActivate]: Fetches fresh state from CDN and waits for completion before becoming ready. + * Use this when you want guaranteed fresh flags at startup. + * - [ActivateAndFetchAsync]: Immediately activates with cached state and fetches updates in background. + * Use this for faster startup when stale values are acceptable. + */ +sealed interface InitialisationStrategy { + /** + * Fetch fresh state from CDN and activate it before becoming ready. + * Provides guaranteed fresh flags but may increase startup latency. + */ + object FetchAndActivate : InitialisationStrategy + + /** + * Immediately activate cached state and fetch updates asynchronously. + * Faster startup but flags may be stale until fetch completes. + */ + object ActivateAndFetchAsync : InitialisationStrategy +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/LocalProviderConfig.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/LocalProviderConfig.kt new file mode 100644 index 00000000..e448fb03 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/LocalProviderConfig.kt @@ -0,0 +1,56 @@ +package com.spotify.confidence.android + +import okhttp3.OkHttpClient +import java.time.Duration + +/** + * Configuration for the Confidence local OpenFeature provider. + * + * @property channelFactory Factory for creating gRPC channels (useful for testing) + * @property httpClient Custom OkHttpClient for CDN requests + * @property useRemoteMaterializationStore Whether to use remote gRPC for sticky assignments + * @property statePollInterval How often to poll for state updates + * @property logFlushInterval How often to flush flag logs + * @property initialisationStrategy Strategy for provider initialization + */ +data class LocalProviderConfig( + val channelFactory: ChannelFactory = DefaultChannelFactory(), + val httpClient: OkHttpClient? = null, + val useRemoteMaterializationStore: Boolean = false, + val statePollInterval: Duration = DEFAULT_POLL_INTERVAL, + val logFlushInterval: Duration = DEFAULT_LOG_FLUSH_INTERVAL, + val initialisationStrategy: InitialisationStrategy = InitialisationStrategy.FetchAndActivate +) { + companion object { + val DEFAULT_POLL_INTERVAL: Duration = Duration.ofSeconds(30) + val DEFAULT_LOG_FLUSH_INTERVAL: Duration = Duration.ofSeconds(10) + } + + /** + * Builder for creating [LocalProviderConfig] instances. + */ + class Builder { + private var channelFactory: ChannelFactory = DefaultChannelFactory() + private var httpClient: OkHttpClient? = null + private var useRemoteMaterializationStore: Boolean = false + private var statePollInterval: Duration = DEFAULT_POLL_INTERVAL + private var logFlushInterval: Duration = DEFAULT_LOG_FLUSH_INTERVAL + private var initialisationStrategy: InitialisationStrategy = InitialisationStrategy.FetchAndActivate + + fun channelFactory(factory: ChannelFactory) = apply { channelFactory = factory } + fun httpClient(client: OkHttpClient) = apply { httpClient = client } + fun useRemoteMaterializationStore(use: Boolean) = apply { useRemoteMaterializationStore = use } + fun statePollInterval(interval: Duration) = apply { statePollInterval = interval } + fun logFlushInterval(interval: Duration) = apply { logFlushInterval = interval } + fun initialisationStrategy(strategy: InitialisationStrategy) = apply { initialisationStrategy = strategy } + + fun build(): LocalProviderConfig = LocalProviderConfig( + channelFactory = channelFactory, + httpClient = httpClient, + useRemoteMaterializationStore = useRemoteMaterializationStore, + statePollInterval = statePollInterval, + logFlushInterval = logFlushInterval, + initialisationStrategy = initialisationStrategy + ) + } +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/MaterializationStore.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/MaterializationStore.kt new file mode 100644 index 00000000..090b078e --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/MaterializationStore.kt @@ -0,0 +1,127 @@ +package com.spotify.confidence.android + +import java.util.Optional +import java.util.concurrent.CompletableFuture + +/** + * Storage abstraction for materialization data used in flag resolution. + * + * Materializations support two key use cases: + * - **Sticky Assignments**: Maintain consistent variant assignments across evaluations + * even when targeting attributes change. + * - **Custom Targeting via Materialized Segments**: Precomputed sets of identifiers + * from datasets that should be targeted. + * + * Default Behavior: By default, the provider uses [UnsupportedMaterializationStore] + * which triggers remote resolution via gRPC to the Confidence service. + * + * Thread Safety: Implementations must be thread-safe as they may be called + * concurrently from multiple threads resolving flags in parallel. + */ +interface MaterializationStore { + + /** + * Performs a batch read of materialization data. + * + * @param ops the list of read operations to perform + * @return a CompletableFuture that completes with the read results + * @throws MaterializationNotSupportedException if the store doesn't support reads + */ + @Throws(MaterializationNotSupportedException::class) + fun read(ops: List): CompletableFuture> + + /** + * Performs a batch write of materialization data. + * + * @param ops the set of write operations to perform + * @return a CompletableFuture that completes when all writes are finished + * @throws MaterializationNotSupportedException by default if not overridden + */ + @Throws(MaterializationNotSupportedException::class) + fun write(ops: Set): CompletableFuture { + throw MaterializationNotSupportedException() + } + + /** + * Represents a write operation to store materialization data. + */ + sealed interface WriteOp { + val materialization: String + val unit: String + + /** + * A variant assignment write operation. + */ + data class Variant( + override val materialization: String, + override val unit: String, + val rule: String, + val variant: String + ) : WriteOp + } + + /** + * Represents the result of a read operation. + */ + sealed interface ReadResult { + val materialization: String + val unit: String + + /** + * Result indicating whether a unit is included in a materialized segment. + */ + data class Inclusion( + override val materialization: String, + override val unit: String, + val included: Boolean + ) : ReadResult + + /** + * Result containing the variant assignment for a unit and rule. + */ + data class Variant( + override val materialization: String, + override val unit: String, + val rule: String, + val variant: Optional + ) : ReadResult + } + + /** + * Represents a read operation to query materialization data. + */ + sealed interface ReadOp { + val materialization: String + val unit: String + + /** + * Query operation to check if a unit is included in a materialized segment. + */ + data class Inclusion( + override val materialization: String, + override val unit: String + ) : ReadOp { + fun toResult(included: Boolean): ReadResult.Inclusion { + return ReadResult.Inclusion(materialization, unit, included) + } + } + + /** + * Query operation to retrieve the variant assignment for a unit and rule. + */ + data class Variant( + override val materialization: String, + override val unit: String, + val rule: String + ) : ReadOp { + fun toResult(variant: Optional): ReadResult.Variant { + return ReadResult.Variant(materialization, unit, rule, variant) + } + } + } +} + +/** + * Exception thrown when materialization operations are not supported. + */ +class MaterializationNotSupportedException : Exception("Materialization not supported") diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ResolverApi.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ResolverApi.kt new file mode 100644 index 00000000..5e565648 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ResolverApi.kt @@ -0,0 +1,35 @@ +package com.spotify.confidence.android + +import com.spotify.confidence.flags.resolver.v1.ResolveFlagsResponse +import com.spotify.confidence.flags.resolver.v1.ResolveWithStickyRequest +import java.util.concurrent.CompletableFuture + +/** + * Interface for the resolver API that handles flag resolution. + */ +internal interface ResolverApi { + /** + * Initializes the resolver with the given state and account ID. + */ + fun init(state: ByteArray, accountId: String) + + /** + * Returns whether the resolver has been initialized. + */ + fun isInitialized(): Boolean + + /** + * Updates the resolver state and flushes any pending logs. + */ + fun updateStateAndFlushLogs(state: ByteArray, accountId: String) + + /** + * Resolves flags with sticky assignment support. + */ + fun resolveWithSticky(request: ResolveWithStickyRequest): CompletableFuture + + /** + * Closes the resolver, flushing any pending logs. + */ + fun close() +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/StateFetcher.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/StateFetcher.kt new file mode 100644 index 00000000..8af3dd40 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/StateFetcher.kt @@ -0,0 +1,101 @@ +package com.spotify.confidence.android + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import rust_guest.SetResolverStateRequest +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + +/** + * Fetches and caches resolver state from the Confidence CDN. + * + * This class handles: + * - Fetching state from the CDN using SHA256(clientSecret) as the path + * - ETag-based conditional GETs to minimize bandwidth + * - Parsing SetResolverStateRequest protobuf from the response + */ +internal class StateFetcher( + private val clientSecret: String, + private val httpClient: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() +) { + private val etagHolder = AtomicReference(null) + private val rawResolverStateHolder = AtomicReference(ByteArray(0)) + private var accountId: String = "" + + companion object { + private const val CDN_BASE_URL = "https://confidence-resolver-state-cdn.spotifycdn.com/" + } + + /** + * Returns the current cached resolver state. + */ + fun provide(): ByteArray = rawResolverStateHolder.get() + + /** + * Returns the current account ID. + */ + fun accountId(): String = accountId + + /** + * Reloads the resolver state from the CDN. + * Uses ETag for conditional GET to avoid re-downloading unchanged state. + */ + suspend fun reload() { + withContext(Dispatchers.IO) { + try { + fetchAndUpdateStateIfChanged() + } catch (e: Exception) { + android.util.Log.w("StateFetcher", "Failed to reload, ignoring reload", e) + } + } + } + + private fun fetchAndUpdateStateIfChanged() { + val cdnUrl = CDN_BASE_URL + sha256Hex(clientSecret) + + val requestBuilder = Request.Builder().url(cdnUrl) + etagHolder.get()?.let { previousEtag -> + requestBuilder.header("If-None-Match", previousEtag) + } + + val response = httpClient.newCall(requestBuilder.build()).execute() + response.use { resp -> + if (resp.code == 304) { + // Not modified + return + } + + if (!resp.isSuccessful) { + throw RuntimeException("Failed to fetch state: HTTP ${resp.code}") + } + + val etag = resp.header("ETag") + val bytes = resp.body?.bytes() ?: throw RuntimeException("Empty response body") + + // Parse SetResolverStateRequest from CDN response + val stateRequest = SetResolverStateRequest.parseFrom(bytes) + this.accountId = stateRequest.accountId + + // Store the state bytes + rawResolverStateHolder.set(stateRequest.state.toByteArray()) + etagHolder.set(etag) + + android.util.Log.i("StateFetcher", "Loaded resolver state for account=$accountId, etag=$etag") + } + } + + private fun sha256Hex(input: String): String { + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(input.toByteArray(StandardCharsets.UTF_8)) + return hash.joinToString("") { byte -> + String.format("%02x", byte) + } + } +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/SwapWasmResolverApi.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/SwapWasmResolverApi.kt new file mode 100644 index 00000000..3114ce33 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/SwapWasmResolverApi.kt @@ -0,0 +1,106 @@ +package com.spotify.confidence.android + +import com.spotify.confidence.flags.resolver.v1.ResolveFlagsResponse +import com.spotify.confidence.flags.resolver.v1.ResolveWithStickyRequest +import com.spotify.confidence.flags.resolver.v1.ResolveWithStickyResponse +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicReference + +/** + * Swap-based resolver API that allows hot-swapping WASM resolver instances. + * + * This implementation: + * - Creates new WASM instances with updated state + * - Atomically swaps out old instances + * - Handles retries for closed instances + */ +internal class SwapWasmResolverApi( + private val flagLogger: WasmFlagLogger, + initialState: ByteArray, + accountId: String, + private val materializationStore: MaterializationStore +) : ResolverApi { + + private val wasmResolverApiRef = AtomicReference() + + companion object { + private const val MAX_CLOSED_RETRIES = 10 + } + + private fun createWasmResolver(): WasmResolver { + return WasmResolver { logData -> flagLogger.write(logData) } + } + + init { + // Create initial instance + val initialInstance = createWasmResolver() + initialInstance.setResolverState(initialState, accountId) + wasmResolverApiRef.set(initialInstance) + } + + override fun init(state: ByteArray, accountId: String) { + updateStateAndFlushLogs(state, accountId) + } + + override fun isInitialized(): Boolean = true + + override fun updateStateAndFlushLogs(state: ByteArray, accountId: String) { + // Create new instance with updated state + val newInstance = createWasmResolver() + newInstance.setResolverState(state, accountId) + + // Get current instance before switching + val oldInstance = wasmResolverApiRef.getAndSet(newInstance) + oldInstance?.close() + } + + override fun close() { + val currentInstance = wasmResolverApiRef.getAndSet(null) + currentInstance?.close() + } + + override fun resolveWithSticky(request: ResolveWithStickyRequest): CompletableFuture { + return resolveWithStickyInternal(request, 0) + } + + private fun resolveWithStickyInternal( + request: ResolveWithStickyRequest, + closedRetries: Int + ): CompletableFuture { + val instance = wasmResolverApiRef.get() + ?: return CompletableFuture.failedFuture(RuntimeException("Resolver is closed")) + + val response = try { + instance.resolveWithSticky(request) + } catch (e: IsClosedException) { + if (closedRetries >= MAX_CLOSED_RETRIES) { + return CompletableFuture.failedFuture( + RuntimeException("Max retries exceeded for IsClosedException: $MAX_CLOSED_RETRIES", e) + ) + } + return resolveWithStickyInternal(request, closedRetries + 1) + } + + return when (response.resolveResultCase) { + ResolveWithStickyResponse.ResolveResultCase.SUCCESS -> { + val success = response.success + // For now, we skip materialization updates since the proto types aren't available + // TODO: Add materialization support when proto types are generated + CompletableFuture.completedFuture(success.response) + } + + ResolveWithStickyResponse.ResolveResultCase.MISSING_MATERIALIZATIONS -> { + // For now, return an error since we can't handle materializations + // TODO: Add materialization support when proto types are generated + android.util.Log.w("SwapWasmResolverApi", "Missing materializations - sticky assignments not supported yet") + CompletableFuture.failedFuture(RuntimeException("Materialization support not implemented")) + } + + ResolveWithStickyResponse.ResolveResultCase.RESOLVERESULT_NOT_SET -> + CompletableFuture.failedFuture(RuntimeException("Invalid response: resolve result not set")) + + else -> + CompletableFuture.failedFuture(RuntimeException("Unhandled response case: ${response.resolveResultCase}")) + } + } +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/TypeMapper.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/TypeMapper.kt new file mode 100644 index 00000000..32f83c93 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/TypeMapper.kt @@ -0,0 +1,168 @@ +@file:OptIn(kotlin.time.ExperimentalTime::class) + +package com.spotify.confidence.android + +import com.google.protobuf.ListValue +import com.google.protobuf.NullValue +import com.google.protobuf.Struct +import com.spotify.confidence.flags.types.v1.FlagSchema +import dev.openfeature.kotlin.sdk.EvaluationContext +import dev.openfeature.kotlin.sdk.Value + +/** + * Utility object for converting between OpenFeature types and Protobuf types. + */ +internal object TypeMapper { + + /** + * Converts a protobuf Value with its schema to an OpenFeature Value. + */ + fun fromProto(value: com.google.protobuf.Value, schema: FlagSchema): Value { + if (schema.schemaTypeCase == FlagSchema.SchemaTypeCase.SCHEMATYPE_NOT_SET) { + throw IllegalArgumentException("schemaType not set in FlagSchema") + } + + val mismatchPrefix = "Mismatch between schema and value:" + + return when (value.kindCase) { + com.google.protobuf.Value.KindCase.NULL_VALUE -> Value.Null + + com.google.protobuf.Value.KindCase.NUMBER_VALUE -> { + when (schema.schemaTypeCase) { + FlagSchema.SchemaTypeCase.INT_SCHEMA -> { + val intVal = value.numberValue.toInt() + if (intVal.toDouble() != value.numberValue) { + throw IllegalArgumentException("$mismatchPrefix value should be an int, but it is a double/long") + } + Value.Integer(intVal) + } + FlagSchema.SchemaTypeCase.DOUBLE_SCHEMA -> Value.Double(value.numberValue) + else -> throw IllegalArgumentException("Number field must have schema type int or double") + } + } + + com.google.protobuf.Value.KindCase.STRING_VALUE -> { + if (schema.schemaTypeCase != FlagSchema.SchemaTypeCase.STRING_SCHEMA) { + throw IllegalArgumentException("$mismatchPrefix value is a String, but it should be something else") + } + Value.String(value.stringValue) + } + + com.google.protobuf.Value.KindCase.BOOL_VALUE -> { + if (schema.schemaTypeCase != FlagSchema.SchemaTypeCase.BOOL_SCHEMA) { + throw IllegalArgumentException("$mismatchPrefix value is a bool, but should be something else") + } + Value.Boolean(value.boolValue) + } + + com.google.protobuf.Value.KindCase.STRUCT_VALUE -> { + if (schema.schemaTypeCase != FlagSchema.SchemaTypeCase.STRUCT_SCHEMA) { + throw IllegalArgumentException("$mismatchPrefix value is a struct, but should be something else") + } + fromProto(value.structValue, schema.structSchema) + } + + com.google.protobuf.Value.KindCase.LIST_VALUE -> { + if (schema.schemaTypeCase != FlagSchema.SchemaTypeCase.LIST_SCHEMA) { + throw IllegalArgumentException("$mismatchPrefix value is a list, but should be something else") + } + val mappedList = value.listValue.valuesList.map { v -> + fromProto(v, schema.listSchema.elementSchema) + } + Value.List(mappedList) + } + + com.google.protobuf.Value.KindCase.KIND_NOT_SET -> + throw IllegalArgumentException("kind not set in com.google.protobuf.Value") + + else -> throw IllegalArgumentException("Unknown value type") + } + } + + /** + * Converts a protobuf Struct with its schema to an OpenFeature Value. + */ + fun fromProto(struct: Struct, schema: FlagSchema.StructFlagSchema): Value { + val map = struct.fieldsMap.entries.associate { (key, value) -> + if (!schema.schemaMap.containsKey(key)) { + throw IllegalArgumentException("Lacking schema for field '$key'") + } + key to fromProto(value, schema.schemaMap[key]!!) + } + return Value.Structure(map) + } + + /** + * Converts an OpenFeature Value to a protobuf Value. + */ + fun toProto(value: Value): com.google.protobuf.Value { + return when (value) { + is Value.Null -> com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build() + + is Value.Boolean -> com.google.protobuf.Value.newBuilder() + .setBoolValue(value.boolean) + .build() + + is Value.Integer -> com.google.protobuf.Value.newBuilder() + .setNumberValue(value.integer.toDouble()) + .build() + + is Value.Double -> com.google.protobuf.Value.newBuilder() + .setNumberValue(value.double) + .build() + + is Value.String -> com.google.protobuf.Value.newBuilder() + .setStringValue(value.string) + .build() + + is Value.List -> com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addAllValues(value.list.map { toProto(it) }) + .build() + ) + .build() + + is Value.Structure -> { + val protoMap = value.structure.entries.associate { (key, v) -> + key to toProto(v) + } + com.google.protobuf.Value.newBuilder() + .setStructValue(Struct.newBuilder().putAllFields(protoMap).build()) + .build() + } + + is Value.Instant -> com.google.protobuf.Value.newBuilder() + .setStringValue(value.instant.toString()) + .build() + } + } + + /** + * Converts an OpenFeature EvaluationContext to a protobuf Struct. + */ + fun evaluationContextToStruct(context: EvaluationContext?): Struct { + if (context == null) { + return Struct.getDefaultInstance() + } + + val builder = Struct.newBuilder() + + // Add targeting key + builder.putFields( + "targeting_key", + com.google.protobuf.Value.newBuilder() + .setStringValue(context.getTargetingKey()) + .build() + ) + + // Add all context attributes + context.asMap().forEach { (key, value) -> + builder.putFields(key, toProto(value)) + } + + return builder.build() + } +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/UnsupportedMaterializationStore.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/UnsupportedMaterializationStore.kt new file mode 100644 index 00000000..f64d2df8 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/UnsupportedMaterializationStore.kt @@ -0,0 +1,18 @@ +package com.spotify.confidence.android + +import java.util.concurrent.CompletableFuture + +/** + * Default materialization store that throws [MaterializationNotSupportedException] + * for all operations, triggering fallback to remote gRPC resolution. + */ +internal class UnsupportedMaterializationStore : MaterializationStore { + + override fun read(ops: List): CompletableFuture> { + throw MaterializationNotSupportedException() + } + + override fun write(ops: Set): CompletableFuture { + throw MaterializationNotSupportedException() + } +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ValueConversions.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ValueConversions.kt new file mode 100644 index 00000000..8cf6a6de --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/ValueConversions.kt @@ -0,0 +1,98 @@ +@file:OptIn(kotlin.time.ExperimentalTime::class) + +package com.spotify.confidence.android + +import dev.openfeature.kotlin.sdk.EvaluationContext +import dev.openfeature.kotlin.sdk.Value +import java.util.Date + +/** + * Extension functions for converting between OpenFeature Value and ConfidenceValue types. + */ + +/** + * Converts an OpenFeature Value to a ConfidenceValue. + */ +fun Value.toConfidenceValue(): ConfidenceValue = when (this) { + is Value.Null -> ConfidenceValue.Null + is Value.Boolean -> ConfidenceValue.Boolean(this.boolean) + is Value.Integer -> ConfidenceValue.Integer(this.integer) + is Value.Double -> ConfidenceValue.Double(this.double) + is Value.String -> ConfidenceValue.String(this.string) + is Value.List -> ConfidenceValue.List(this.list.map { it.toConfidenceValue() }) + is Value.Structure -> ConfidenceValue.Struct(this.structure.mapValues { it.value.toConfidenceValue() }) + is Value.Instant -> ConfidenceValue.Timestamp(Date(this.instant.epochSeconds * 1000)) +} + +/** + * Converts a ConfidenceValue to an OpenFeature Value. + */ +fun ConfidenceValue.toValue(): Value = when (this) { + is ConfidenceValue.Boolean -> Value.Boolean(this.boolean) + is ConfidenceValue.Double -> Value.Double(this.double) + is ConfidenceValue.Integer -> Value.Integer(this.integer) + is ConfidenceValue.List -> Value.List(this.list.map { it.toValue() }) + ConfidenceValue.Null -> Value.Null + is ConfidenceValue.String -> Value.String(this.string) + is ConfidenceValue.Struct -> Value.Structure(this.map.mapValues { it.value.toValue() }) + is ConfidenceValue.Timestamp -> Value.Instant( + kotlin.time.Instant.fromEpochMilliseconds(this.dateTime.time) + ) +} + +/** + * Converts an EvaluationContext to a ConfidenceValue.Struct. + */ +fun EvaluationContext.toConfidenceContext(): ConfidenceValue.Struct { + val map = mutableMapOf() + + // Add targeting key + map["targeting_key"] = ConfidenceValue.String(getTargetingKey()) + + // Add all attributes + asMap().forEach { (key, value) -> + map[key] = value.toConfidenceValue() + } + + return ConfidenceValue.Struct(map) +} + +/** + * Extracts a value at the given path from a ConfidenceValue.Struct. + */ +fun findValueFromPath(value: ConfidenceValue.Struct, path: List): ConfidenceValue? { + if (path.isEmpty()) return value + + val currValue = value.map[path[0]] ?: return null + + return when { + currValue is ConfidenceValue.Struct && path.size > 1 -> { + findValueFromPath(currValue, path.subList(1, path.size)) + } + path.size == 1 -> currValue + else -> null + } +} + +/** + * Converts a ResolveReason to an OpenFeature reason string. + */ +fun ResolveReason.toOpenFeatureReason(): String = when (this) { + ResolveReason.RESOLVE_REASON_MATCH -> "TARGETING_MATCH" + ResolveReason.RESOLVE_REASON_STALE -> "STALE" + ResolveReason.ERROR -> "ERROR" + ResolveReason.RESOLVE_REASON_TARGETING_KEY_ERROR -> "ERROR" + ResolveReason.RESOLVE_REASON_UNSPECIFIED -> "UNKNOWN" + else -> "DEFAULT" +} + +/** + * Converts an ErrorCode to an OpenFeature error code. + */ +fun ErrorCode.toOpenFeatureErrorCode(): dev.openfeature.kotlin.sdk.exceptions.ErrorCode = when (this) { + ErrorCode.FLAG_NOT_FOUND -> dev.openfeature.kotlin.sdk.exceptions.ErrorCode.FLAG_NOT_FOUND + ErrorCode.INVALID_CONTEXT -> dev.openfeature.kotlin.sdk.exceptions.ErrorCode.INVALID_CONTEXT + ErrorCode.PARSE_ERROR -> dev.openfeature.kotlin.sdk.exceptions.ErrorCode.PARSE_ERROR + ErrorCode.PROVIDER_NOT_READY -> dev.openfeature.kotlin.sdk.exceptions.ErrorCode.PROVIDER_NOT_READY + ErrorCode.GENERAL -> dev.openfeature.kotlin.sdk.exceptions.ErrorCode.GENERAL +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/Version.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/Version.kt new file mode 100644 index 00000000..20ed2743 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/Version.kt @@ -0,0 +1,8 @@ +package com.spotify.confidence.android + +/** + * SDK version information for the Confidence Android OpenFeature provider. + */ +internal object Version { + const val VERSION = "0.1.0" +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/WasmFlagLogger.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/WasmFlagLogger.kt new file mode 100644 index 00000000..4415614c --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/WasmFlagLogger.kt @@ -0,0 +1,21 @@ +package com.spotify.confidence.android + +/** + * Interface for logging flag resolution events to the Confidence service. + */ +internal interface WasmFlagLogger { + /** + * Asynchronously writes flag logs (raw bytes from WASM). + */ + fun write(logData: ByteArray) + + /** + * Synchronously writes flag logs, blocking until complete. + */ + fun writeSync(logData: ByteArray) + + /** + * Shuts down the logger, waiting for pending writes to complete. + */ + fun shutdown() +} diff --git a/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/WasmResolver.kt b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/WasmResolver.kt new file mode 100644 index 00000000..725aad38 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/kotlin/com/spotify/confidence/android/WasmResolver.kt @@ -0,0 +1,243 @@ +package com.spotify.confidence.android + +import com.dylibso.chicory.runtime.ExportFunction +import com.dylibso.chicory.runtime.ImportFunction +import com.dylibso.chicory.runtime.ImportValues +import com.dylibso.chicory.runtime.Instance +import com.dylibso.chicory.runtime.Memory +import com.dylibso.chicory.wasm.types.ValType +import com.spotify.confidence.wasm.ConfidenceResolver +import com.google.protobuf.ByteString +import com.google.protobuf.InvalidProtocolBufferException +import com.google.protobuf.MessageLite +import com.google.protobuf.Timestamp +import com.spotify.confidence.flags.resolver.v1.ResolveWithStickyRequest +import com.spotify.confidence.flags.resolver.v1.ResolveWithStickyResponse +import rust_guest.LogMessage +import rust_guest.Request +import rust_guest.Response +import rust_guest.SetResolverStateRequest +import rust_guest.Void as WasmVoid +import java.time.Instant +import java.util.concurrent.locks.ReentrantReadWriteLock + +/** + * WASM resolver that interfaces with the Confidence resolver WASM binary using Chicory runtime. + * + * This class handles all WASM memory management and provides methods for: + * - Setting resolver state + * - Resolving flags with sticky assignments + * - Flushing logs + */ +internal class WasmResolver( + private val onFlushLogs: (ByteArray) -> Unit +) { + private val hostParamTypes = listOf(ValType.I32) + private val hostReturnTypes = listOf(ValType.I32) + private val instance: Instance + private val wasmLock = ReentrantReadWriteLock() + + @Volatile + private var isConsumed = false + + // WASM memory interop functions + private val wasmMsgAlloc: ExportFunction + private val wasmMsgFree: ExportFunction + + // WASM API functions + private val wasmMsgGuestSetResolverState: ExportFunction + private val wasmMsgFlushLogs: ExportFunction + private val wasmMsgGuestResolveWithSticky: ExportFunction + + init { + // Use pre-compiled WASM module for better performance on Android + // The module is compiled at build time using Chicory's build-time compiler + val module = ConfidenceResolver.load() + instance = Instance.builder(module) + .withImportValues( + ImportValues.builder() + .addFunction(createImportFunction("current_time", WasmVoid::parseFrom, ::currentTime)) + .addFunction(createImportFunction("log_message", LogMessage::parseFrom, ::log)) + .addFunction( + ImportFunction( + "wasm_msg", + "wasm_msg_current_thread_id", + emptyList(), + listOf(ValType.I32) + ) { _, _ -> longArrayOf(0) } + ) + .build() + ) + // Use pre-compiled machine factory for fast execution + .withMachineFactory(ConfidenceResolver::create) + .build() + + wasmMsgAlloc = instance.export("wasm_msg_alloc") + wasmMsgFree = instance.export("wasm_msg_free") + wasmMsgGuestSetResolverState = instance.export("wasm_msg_guest_set_resolver_state") + wasmMsgFlushLogs = instance.export("wasm_msg_guest_flush_logs") + wasmMsgGuestResolveWithSticky = instance.export("wasm_msg_guest_resolve_with_sticky") + } + + private fun log(message: LogMessage): MessageLite { + android.util.Log.d("WasmResolver", message.message) + return WasmVoid.getDefaultInstance() + } + + private fun currentTime(@Suppress("UNUSED_PARAMETER") unused: WasmVoid): Timestamp { + return Timestamp.newBuilder() + .setSeconds(Instant.now().epochSecond) + .build() + } + + /** + * Sets the resolver state from CDN. + */ + fun setResolverState(state: ByteArray, accountId: String) { + val resolverStateRequest = SetResolverStateRequest.newBuilder() + .setState(ByteString.copyFrom(state)) + .setAccountId(accountId) + .build() + + val request = Request.newBuilder() + .setData(ByteString.copyFrom(resolverStateRequest.toByteArray())) + .build() + .toByteArray() + + val addr = transfer(request) + val respPtr = wasmMsgGuestSetResolverState.apply(addr.toLong())[0].toInt() + consumeResponse(respPtr) { WasmVoid.parseFrom(it) } + } + + /** + * Resolves flags with sticky assignment support. + */ + @Throws(IsClosedException::class) + fun resolveWithSticky(request: ResolveWithStickyRequest): ResolveWithStickyResponse { + if (!wasmLock.writeLock().tryLock() || isConsumed) { + throw IsClosedException() + } + try { + val reqPtr = transferRequest(request) + val respPtr = wasmMsgGuestResolveWithSticky.apply(reqPtr.toLong())[0].toInt() + return consumeResponse(respPtr) { ResolveWithStickyResponse.parseFrom(it) } + } finally { + wasmLock.writeLock().unlock() + } + } + + /** + * Flushes pending logs and closes the resolver. + */ + fun close() { + wasmLock.readLock().lock() + try { + val voidRequest = WasmVoid.getDefaultInstance() + val reqPtr = transferRequest(voidRequest) + val respPtr = wasmMsgFlushLogs.apply(reqPtr.toLong())[0].toInt() + val responseBytes = consumeResponseBytes(respPtr) + onFlushLogs(responseBytes) + isConsumed = true + } finally { + wasmLock.readLock().unlock() + } + } + + private fun consumeResponse(addr: Int, codec: (ByteArray) -> T): T { + try { + val response = Response.parseFrom(consume(addr)) + if (response.hasError()) { + throw RuntimeException(response.error) + } + return codec(response.data.toByteArray()) + } catch (e: InvalidProtocolBufferException) { + throw RuntimeException(e) + } + } + + private fun consumeResponseBytes(addr: Int): ByteArray { + try { + val response = Response.parseFrom(consume(addr)) + if (response.hasError()) { + throw RuntimeException(response.error) + } + return response.data.toByteArray() + } catch (e: InvalidProtocolBufferException) { + throw RuntimeException(e) + } + } + + private fun transferRequest(message: MessageLite): Int { + val request = Request.newBuilder() + .setData(ByteString.copyFrom(message.toByteArray())) + .build() + .toByteArray() + return transfer(request) + } + + private fun transferResponseSuccess(response: MessageLite): Int { + val wrapperBytes = Response.newBuilder() + .setData(ByteString.copyFrom(response.toByteArray())) + .build() + .toByteArray() + return transfer(wrapperBytes) + } + + private fun transferResponseError(error: String): Int { + val wrapperBytes = Response.newBuilder() + .setError(error) + .build() + .toByteArray() + return transfer(wrapperBytes) + } + + private fun consume(addr: Int): ByteArray { + val mem: Memory = instance.memory() + val len = (mem.readU32(addr - 4) - 4L).toInt() + val data = mem.readBytes(addr, len) + wasmMsgFree.apply(addr.toLong()) + return data + } + + private fun transfer(data: ByteArray): Int { + val mem: Memory = instance.memory() + val addr = wasmMsgAlloc.apply(data.size.toLong())[0].toInt() + mem.write(addr, data) + return addr + } + + private fun consumeRequest(addr: Int, codec: (ByteArray) -> T): T { + try { + val request = Request.parseFrom(consume(addr)) + return codec(request.data.toByteArray()) + } catch (e: InvalidProtocolBufferException) { + throw RuntimeException(e) + } + } + + private fun createImportFunction( + name: String, + reqCodec: (ByteArray) -> T, + impl: (T) -> MessageLite + ): ImportFunction { + return ImportFunction( + "wasm_msg", + "wasm_msg_host_$name", + hostParamTypes, + hostReturnTypes + ) { _, args -> + try { + val message = consumeRequest(args[0].toInt(), reqCodec) + val response = impl(message) + longArrayOf(transferResponseSuccess(response).toLong()) + } catch (e: Exception) { + longArrayOf(transferResponseError(e.message ?: "Unknown error").toLong()) + } + } + } +} + +/** + * Exception thrown when the resolver is closed and cannot process more requests. + */ +class IsClosedException : Exception("Resolver is closed") diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/confidence/api/annotations.proto b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/api/annotations.proto new file mode 100644 index 00000000..173c9751 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/api/annotations.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package confidence.api; + +import "google/protobuf/descriptor.proto"; + +option java_multiple_files = true; +option java_outer_classname = "ApiAnnotationsProto"; +option java_package = "com.spotify.confidence.api"; + +message Resource { + string type = 1; +} + +// Rate limit specification that is applied on global rates per account +message RateLimitSpec { + int32 rps_limit = 1; +} + +extend google.protobuf.MethodOptions { + Resource resource_method = 4399226; + RateLimitSpec rate_limit = 4399227; +} + +extend google.protobuf.ServiceOptions { + string service_name = 4399229; + repeated string hosts = 4399228; + RateLimitSpec service_rate_limit = 4399230; +} + +message ValidationSpec { + string regex = 1; +} + +extend google.protobuf.FieldOptions { + ValidationSpec validation = 4324223; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/api.proto b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/api.proto new file mode 100644 index 00000000..d272f0ec --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/api.proto @@ -0,0 +1,183 @@ +syntax = "proto3"; + +package confidence.flags.resolver.v1; + +import "google/api/resource.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; + +import "confidence/api/annotations.proto"; +import "confidence/flags/types/v1/types.proto"; +import "confidence/flags/resolver/v1/types.proto"; + +option java_package = "com.spotify.confidence.flags.resolver.v1"; +option java_multiple_files = true; +option java_outer_classname = "ApiProto"; + +// The service that allows a client to resolve a flag into a variant and its +// corresponding value. +service FlagResolverService { + option (confidence.api.service_name) = "Resolver"; + option (confidence.api.hosts) = "resolver.eu.confidence.dev"; + option (confidence.api.hosts) = "resolver.us.confidence.dev"; + + // Resolve multiple flags into variants and values. This method resolves + // all flags that are enabled for the given client, or a subset of them + // specified in the request. + // A flag is resolved by evaluating its rules in order, a rule matches if: + // 1) it is enabled, 2) the referred segment is active, and 3) the + // randomization unit is in the population indicated by the segment's + // targeting criteria and population allocation. The first rule that matches + // will assign a variant and value to the unit. Archived flags are not included. + rpc ResolveFlags(ResolveFlagsRequest) returns (ResolveFlagsResponse) { + option (google.api.http) = { + post: "/v1/flags:resolve" + body: "*" + }; + option (confidence.api.resource_method).type = "flags.confidence.dev/Flag"; + } + + // Indicates that resolved values of a set of flags have been used. In many + // situations there is a delay between the moment a flag is resolved and + // when it is actually used in a client. This is often the case in mobile + // clients where you typically batch resolve all flags at startup, but then + // apply them later when the user interacts with a specific view. If the + // `apply` flag is set to false in a resolve, the flag assignment event is + // delayed until the flag is applied. + rpc ApplyFlags(ApplyFlagsRequest) returns (ApplyFlagsResponse) { + option (google.api.http) = { + post: "/v1/flags:apply" + body: "*" + }; + option (confidence.api.resource_method).type = "flags.confidence.dev/Flag"; + } +} + +message ResolveFlagsRequest { + // If non-empty, the specific flags are resolved, otherwise all flags + // available to the client will be resolved. + repeated string flags = 1 [ + (google.api.resource_reference).type = "flags.confidence.dev/Flag", + (google.api.field_behavior) = OPTIONAL + ]; + + // An object that contains data used in the flag resolve. For example, + // the targeting key e.g. the id of the randomization unit, other attributes + // like country or version that are used for targeting. + google.protobuf.Struct evaluation_context = 2 [ + (google.api.field_behavior) = OPTIONAL + ]; + + // Credentials for the client. It is used to identify the client and find + // the flags that are available to it. + string client_secret = 3 [ + (google.api.field_behavior) = REQUIRED + ]; + + // Determines whether the flags should be applied directly as part of the + // resolve, or delayed until `ApplyFlag` is called. A flag is typically + // applied when it is used, if this occurs much later than the resolve, then + // `apply` should likely be set to false. + bool apply = 4 [ + (google.api.field_behavior) = REQUIRED + ]; + + // Information about the SDK used to initiate the request. + Sdk sdk = 5 [ + (google.api.field_behavior) = OPTIONAL + ]; +} + +message ResolveFlagsResponse { + // The list of all flags that could be resolved. Note: if any flag was + // archived it will not be included in this list. + repeated ResolvedFlag resolved_flags = 1; + + // An opaque token that is used when `apply` is set to false in `ResolveFlags`. + // When `apply` is set to false, the token must be passed to `ApplyFlags`. + bytes resolve_token = 2; + + // Unique identifier for this particular resolve request. + string resolve_id = 3; +} + +message ApplyFlagsRequest { + // The flags to apply and information about when they were applied. + repeated AppliedFlag flags = 1 [ + (google.api.field_behavior) = REQUIRED + ]; + + // Credentials for the client. + string client_secret = 2 [ + (google.api.field_behavior) = REQUIRED + ]; + + // An opaque token that was returned from `ResolveFlags`; it must be set. + bytes resolve_token = 3 [ + (google.api.field_behavior) = REQUIRED + ]; + + + // The client time when the this request was sent, used for correcting + // clock skew from the client. + google.protobuf.Timestamp send_time = 4 [ + (google.api.field_behavior) = REQUIRED + ]; + + // Information about the SDK used to initiate the request. + Sdk sdk = 5 [ + (google.api.field_behavior) = OPTIONAL + ]; +} + +message ApplyFlagsResponse { + +} + +message AppliedFlag { + // The id of the flag that should be applied, has the format `flags/*`. + string flag = 1 [ + (google.api.resource_reference).type = "flags.confidence.dev/Flag", + (google.api.field_behavior) = REQUIRED + ]; + + // The client time when the flag was applied. + google.protobuf.Timestamp apply_time = 2 [ + (google.api.field_behavior) = REQUIRED + ]; +} + +message ResolvedFlag { + // The id of the flag that as resolved. + string flag = 1 [ + (google.api.resource_reference).type = "flags.confidence.dev/Flag" + ]; + + // The id of the resolved variant has the format `flags/abc/variants/xyz`. + string variant = 2 [ + (google.api.resource_reference).type = "flags.confidence.dev/Variant" + ]; + + // The value corresponding to the variant. It will always be a json object, + // for example `{ "color": "red", "size": 12 }`. + google.protobuf.Struct value = 3; + + // The schema of the value that was returned. For example: + // ``` + // { + // "schema": { + // "color": { "stringSchema": {} }, + // "size": { "intSchema": {} } + // } + // } + // ``` + types.v1.FlagSchema.StructFlagSchema flag_schema = 4; + + // The reason to why the flag could be resolved or not. + ResolveReason reason = 5; + + // Determines whether the flag should be applied in the clients + bool should_apply = 6 [(google.api.field_behavior) = OUTPUT_ONLY]; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/internal_api.proto b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/internal_api.proto new file mode 100644 index 00000000..758c0542 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/internal_api.proto @@ -0,0 +1,189 @@ +syntax = "proto3"; + +// IMPORTANT: Package name must match backend expectation for gRPC service discovery +// The full gRPC service name is: confidence.flags.resolver.v1.InternalFlagLoggerService +package confidence.flags.resolver.v1; + +import "google/protobuf/timestamp.proto"; +import "confidence/flags/resolver/v1/types.proto"; + +option go_package = "github.com/spotify/confidence-resolver/openfeature-provider/go/confidence/internal/proto/resolverinternal"; +option java_package = "com.spotify.confidence.flags.resolver.v1"; +option java_multiple_files = true; +option java_outer_classname = "InternalApiProto"; + +// Simplified from confidence-resolver/protos/confidence/flags/resolver/v1/internal_api.proto +// Annotations removed for minimal dependencies + +service InternalFlagLoggerService { + // Client writes flag assignment events and resolve logs using client secret authentication. + rpc ClientWriteFlagLogs(WriteFlagLogsRequest) returns (WriteFlagLogsResponse); + + // Stores materializations for units. Uses client secret authentication. + rpc WriteMaterializedOperations(WriteOperationsRequest) returns (WriteOperationsResult) {} + + // Loads materializations for units. Uses client secret authentication. + rpc ReadMaterializedOperations(ReadOperationsRequest) returns (ReadOperationsResult) {} +} + +// The service that allows to report flag assigned and other client-side flag +// operations, useful when the resolve engine runs on the customer's premises +message WriteFlagLogsRequest { + repeated FlagAssigned flag_assigned = 1; + + TelemetryData telemetry_data = 2; + + repeated ClientResolveInfo client_resolve_info = 3; + repeated FlagResolveInfo flag_resolve_info = 4; +} + +message WriteFlagLogsResponse {} + +// Collection of telemetry metrics +message TelemetryData { + // Information about the SDK/provider + Sdk sdk = 2; +} + +message ClientInfo { + string client = 1; + string client_credential = 2; + + // Information about the SDK used to interact with the API. + Sdk sdk = 3; +} + +message FlagAssigned { + string resolve_id = 10; + + ClientInfo client_info = 3; + + repeated AppliedFlag flags = 15; + + message AppliedFlag { + string flag = 1; + + string targeting_key = 2; + string targeting_key_selector = 3; + + oneof assignment { + AssignmentInfo assignment_info = 4; + DefaultAssignment default_assignment = 5; + } + + string assignment_id = 6; + + string rule = 7; + + repeated FallthroughAssignment fallthrough_assignments = 8; + google.protobuf.Timestamp apply_time = 9; + } + + message AssignmentInfo { + string segment = 1; + string variant = 2; + } + + message DefaultAssignment { + DefaultAssignmentReason reason = 1; + enum DefaultAssignmentReason { + DEFAULT_ASSIGNMENT_REASON_UNSPECIFIED = 0; + NO_SEGMENT_MATCH = 1; + NO_TREATMENT_MATCH = 2 [deprecated = true]; + FLAG_ARCHIVED = 3; + } + } +} + +message FallthroughAssignment { + string rule = 1; + + string assignment_id = 2; + + string targeting_key = 3; + string targeting_key_selector = 4; +} + +message ClientResolveInfo { + // Resource reference to a client. + string client = 1; + + // Resource reference to a credential. + string client_credential = 2; + + // The different evaluation context schema of the client that have been seen recently. + repeated EvaluationContextSchemaInstance schema = 3; + + // An instance of a schema that was seen + message EvaluationContextSchemaInstance { + // Schema of each field in the evaluation context. + map schema = 1; + } +} + +message FlagResolveInfo { + // The flag the info is about + string flag = 1; + // Information about how variants were resolved. + repeated VariantResolveInfo variant_resolve_info = 2; + + // Information about how a variant was resolved. + message VariantResolveInfo { + // If there was a variant assigned, otherwise not set + string variant = 1; + // Number of times the variant was resolved in this period + int64 count = 3; + } +} + +message WriteOperationsRequest { + repeated VariantData store_variant_op = 1; +} + +message WriteOperationsResult {} + +message VariantReadOp { + string unit = 1; + string materialization = 2; + string rule = 3; +} + +message InclusionReadOp { + string unit = 1; + string materialization = 2; +} + +message ReadOp { + oneof op { + VariantReadOp variant_read_op = 1; + InclusionReadOp inclusion_read_op = 2; + } +} + +message ReadOperationsRequest { + repeated ReadOp ops = 3; +} + +message VariantData { + string unit = 1; + string materialization = 2; + string rule = 3; + string variant = 4; +} + +message InclusionData { + string unit = 1; + string materialization = 2; + bool is_included = 3; +} + +message ReadResult { + oneof result { + VariantData variant_result = 1; + InclusionData inclusion_result = 2; + } +} + +message ReadOperationsResult { + repeated ReadResult results = 1; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/types.proto b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/types.proto new file mode 100644 index 00000000..8fc25258 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/types.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package confidence.flags.resolver.v1; + +import "google/api/field_behavior.proto"; + +option java_package = "com.spotify.confidence.flags.resolver.v1"; +option java_multiple_files = true; +option java_outer_classname = "TypesProto"; + +// (-- api-linter: core::0123::resource-annotation=disabled +// aip.dev/not-precedent: SDKs are not internal Confidence resources. --) +message Sdk { + // Identifier of the SDK used to interact with the API. + oneof sdk { + // Name of a Confidence SDKs. + SdkId id = 1; + // Custom name for non-Confidence SDKs. + string custom_id = 2; + } + + // Version of the SDK. + string version = 3 [ + (google.api.field_behavior) = OPTIONAL // TODO: Make REQUIRED again when we're not SDK if default + ]; +} + +enum ResolveReason { + // Unspecified enum. + RESOLVE_REASON_UNSPECIFIED = 0; + // The flag was successfully resolved because one rule matched. + RESOLVE_REASON_MATCH = 1; + // The flag could not be resolved because no rule matched. + RESOLVE_REASON_NO_SEGMENT_MATCH = 2; + // The flag could not be resolved because the matching rule had no variant + // that could be assigned. + RESOLVE_REASON_NO_TREATMENT_MATCH = 3 [deprecated = true]; + // The flag could not be resolved because it was archived. + RESOLVE_REASON_FLAG_ARCHIVED = 4; + // The flag could not be resolved because the targeting key field was invalid + RESOLVE_REASON_TARGETING_KEY_ERROR = 5; + // Unknown error occurred during the resolve + RESOLVE_REASON_ERROR = 6; +} + +enum SdkId { + SDK_ID_UNSPECIFIED = 0; + SDK_ID_JAVA_PROVIDER = 1; + SDK_ID_KOTLIN_PROVIDER = 2; + SDK_ID_SWIFT_PROVIDER = 3; + SDK_ID_JS_WEB_PROVIDER = 4; + SDK_ID_JS_SERVER_PROVIDER = 5; + SDK_ID_PYTHON_PROVIDER = 6; + SDK_ID_GO_PROVIDER = 7; + SDK_ID_RUBY_PROVIDER = 8; + SDK_ID_RUST_PROVIDER = 9; + SDK_ID_JAVA_CONFIDENCE = 10; + SDK_ID_KOTLIN_CONFIDENCE = 11; + SDK_ID_SWIFT_CONFIDENCE = 12; + SDK_ID_JS_CONFIDENCE = 13; + SDK_ID_PYTHON_CONFIDENCE = 14; + SDK_ID_GO_CONFIDENCE = 15; + SDK_ID_RUST_CONFIDENCE = 16; + SDK_ID_FLUTTER_IOS_CONFIDENCE = 17; + SDK_ID_FLUTTER_ANDROID_CONFIDENCE = 18; + SDK_ID_DOTNET_CONFIDENCE = 19; + SDK_ID_GO_LOCAL_PROVIDER = 20; + SDK_ID_JAVA_LOCAL_PROVIDER = 21; + SDK_ID_JS_LOCAL_SERVER_PROVIDER = 22; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/wasm_api.proto b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/wasm_api.proto new file mode 100644 index 00000000..63ecf6bb --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/resolver/v1/wasm_api.proto @@ -0,0 +1,75 @@ +syntax = "proto3"; + +package confidence.flags.resolver.v1; + +import "google/api/resource.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; + +import "confidence/api/annotations.proto"; +import "confidence/flags/types/v1/types.proto"; +import "confidence/flags/resolver/v1/types.proto"; +import "confidence/flags/resolver/v1/api.proto"; + +option java_package = "com.spotify.confidence.flags.resolver.v1"; +option java_multiple_files = true; +option java_outer_classname = "WasmApiProto"; + + +message ResolveWithStickyRequest { + ResolveFlagsRequest resolve_request = 1; + + // Context about the materialization required for the resolve + map materializations_per_unit = 2; + + // if a materialization info is missing, we want tor return to the caller immediately + bool fail_fast_on_sticky = 3; + // if we should support sticky or completely skip the flag if they had sticky rules + bool not_process_sticky = 4; +} + +message MaterializationMap { + // materialization name to info + map info_map = 1; +} + +message MaterializationInfo { + bool unit_in_info = 1; + map rule_to_variant = 2; +} + +message LogMessage { + string message = 1; +} + +message ResolveWithStickyResponse { + oneof resolve_result { + Success success = 1; + MissingMaterializations missing_materializations = 2; + } + + message Success { + ResolveFlagsResponse response = 1; + repeated MaterializationUpdate updates = 2; + } + + message MissingMaterializations { + repeated MissingMaterializationItem items = 1; + } + + message MissingMaterializationItem { + string unit = 1; + string rule = 2; + string read_materialization = 3; + } + + message MaterializationUpdate { + string unit = 1; + string write_materialization = 2; + string rule = 3; + string variant = 4; + } +} + diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/types/v1/types.proto b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/types/v1/types.proto new file mode 100644 index 00000000..88d67e10 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/confidence/flags/types/v1/types.proto @@ -0,0 +1,73 @@ +syntax = "proto3"; + +package confidence.flags.types.v1; + +option java_package = "com.spotify.confidence.flags.types.v1"; +option java_multiple_files = true; +option java_outer_classname = "TypesProto"; + +// Schema for the value of a flag. +// +// The value of a flag is always a struct with one or more nested fields. +// Example of a struct schema with two fields, `color` (a string) and `len` (an int): +// +// ``` +// { +// "schema": { +// "color": { +// "stringSchema": {} +// }, +// "len": { +// "intSchema": {} +// } +// } +// } +// ``` +message FlagSchema { + oneof schema_type { + // Schema if this is a struct + StructFlagSchema struct_schema = 1; + // Schema if this is a list + ListFlagSchema list_schema = 2; + // Schema if this is an int + IntFlagSchema int_schema = 3; + // Schema if this is a double + DoubleFlagSchema double_schema = 4; + //Schema if this is a string + StringFlagSchema string_schema = 5; + //Schema if this is a bool + BoolFlagSchema bool_schema = 6; + } + + // A schema of nested fields. The length of the field name is limited to + // 32 characters and can only contain alphanumeric characters, hyphens and + // underscores. The number of fields in a struct is limited to 64. + // Structs can not be nested more than four (4) levels. + message StructFlagSchema { + // Map of field name to the schema for the field + map schema = 1; + } + + // A number that has a decimal place. + message DoubleFlagSchema { + } + + // A whole number without a decimal point. + message IntFlagSchema { + } + + // A string. The length is limited to 250 characters. + message StringFlagSchema { + } + + // A boolean: true or false. + message BoolFlagSchema { + } + + // A list of values. The values have the same data types which + // is defined by `element_schema`. + message ListFlagSchema { + // The schema for the elements in the list + FlagSchema element_schema = 1; + } +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/api/annotations.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/annotations.proto new file mode 100644 index 00000000..efdab3db --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 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. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/api/client.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/client.proto new file mode 100644 index 00000000..3b3fd0c4 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/client.proto @@ -0,0 +1,99 @@ +// Copyright 2018 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. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "ClientProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // A definition of a client library method signature. + // + // In client libraries, each proto RPC corresponds to one or more methods + // which the end user is able to call, and calls the underlying RPC. + // Normally, this method receives a single argument (a struct or instance + // corresponding to the RPC request object). Defining this field will + // add one or more overloads providing flattened or simpler method signatures + // in some languages. + // + // The fields on the method signature are provided as a comma-separated + // string. + // + // For example, the proto RPC and annotation: + // + // rpc CreateSubscription(CreateSubscriptionRequest) + // returns (Subscription) { + // option (google.api.method_signature) = "name,topic"; + // } + // + // Would add the following Java overload (in addition to the method accepting + // the request object): + // + // public final Subscription createSubscription(String name, String topic) + // + // The following backwards-compatibility guidelines apply: + // + // * Adding this annotation to an unannotated method is backwards + // compatible. + // * Adding this annotation to a method which already has existing + // method signature annotations is backwards compatible if and only if + // the new method signature annotation is last in the sequence. + // * Modifying or removing an existing method signature annotation is + // a breaking change. + // * Re-ordering existing method signature annotations is a breaking + // change. + repeated string method_signature = 1051; +} + +extend google.protobuf.ServiceOptions { + // The hostname for this service. + // This should be specified with no prefix or protocol. + // + // Example: + // + // service Foo { + // option (google.api.default_host) = "foo.googleapi.com"; + // ... + // } + string default_host = 1049; + + // OAuth scopes needed for the client. + // + // Example: + // + // service Foo { + // option (google.api.oauth_scopes) = \ + // "https://www.googleapis.com/auth/cloud-platform"; + // ... + // } + // + // If there is more than one scope, use a comma-separated string: + // + // Example: + // + // service Foo { + // option (google.api.oauth_scopes) = \ + // "https://www.googleapis.com/auth/cloud-platform," + // "https://www.googleapis.com/auth/monitoring"; + // ... + // } + string oauth_scopes = 1050; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/api/field_behavior.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/field_behavior.proto new file mode 100644 index 00000000..344cb0b1 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/field_behavior.proto @@ -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. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated google.api.FieldBehavior field_behavior = 1052; +} + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; + + // Denotes that a (repeated) field is an unordered list. + // This indicates that the service may provide the elements of the list + // in any arbitrary order, rather than the order the user originally + // provided. Additionally, the list's order may or may not be stable. + UNORDERED_LIST = 6; + + // Denotes that this field returns a non-empty default value if not set. + // This indicates that if the user provides the empty value in a request, + // a non-empty value will be returned. The user will not be aware of what + // non-empty value to expect. + NON_EMPTY_DEFAULT = 7; + + // Denotes that the field in a resource (a message annotated with + // google.api.resource) is used in the resource name to uniquely identify the + // resource. For AIP-compliant APIs, this should only be applied to the + // `name` field on the resource. + // + // This behavior should not be applied to references to other resources within + // the message. + // + // The identifier field of resources often have different field behavior + // depending on the request it is embedded in (e.g. for Create methods name + // is optional and unused, while for Update methods it is required). Instead + // of method-specific annotations, only `IDENTIFIER` is required. + IDENTIFIER = 8; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/api/http.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/http.proto new file mode 100644 index 00000000..113fa936 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/http.proto @@ -0,0 +1,375 @@ +// Copyright 2015 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. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/api/httpbody.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/httpbody.proto new file mode 100644 index 00000000..028fa5db --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/httpbody.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package google.api; + +import "google/protobuf/any.proto"; + +option java_package = "com.google.api"; +option java_multiple_files = true; +option java_outer_classname = "HttpBodyProto"; + +// Minimal HttpBody to satisfy imports during local builds +message HttpBody { + string content_type = 1; + bytes data = 2; + repeated google.protobuf.Any extensions = 3; +} + + diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/api/resource.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/resource.proto new file mode 100644 index 00000000..0ce0344f --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/resource.proto @@ -0,0 +1,238 @@ +// Copyright 2018 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. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "ResourceProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // An annotation that describes a resource reference, see + // [ResourceReference][]. + google.api.ResourceReference resource_reference = 1055; +} + +extend google.protobuf.FileOptions { + // An annotation that describes a resource definition without a corresponding + // message; see [ResourceDescriptor][]. + repeated google.api.ResourceDescriptor resource_definition = 1053; +} + +extend google.protobuf.MessageOptions { + // An annotation that describes a resource definition, see + // [ResourceDescriptor][]. + google.api.ResourceDescriptor resource = 1053; +} + +// A simple descriptor of a resource type. +// +// ResourceDescriptor annotates a resource message (either by means of a +// protobuf annotation or use in the service config), and associates the +// resource's schema, the resource type, and the pattern of the resource name. +// +// Example: +// +// message Topic { +// // Indicates this message defines a resource schema. +// // Declares the resource type in the format of {service}/{kind}. +// // For Kubernetes resources, the format is {api group}/{kind}. +// option (google.api.resource) = { +// type: "pubsub.googleapis.com/Topic" +// pattern: "projects/{project}/topics/{topic}" +// }; +// } +// +// The ResourceDescriptor Yaml config will look like: +// +// resources: +// - type: "pubsub.googleapis.com/Topic" +// pattern: "projects/{project}/topics/{topic}" +// +// Sometimes, resources have multiple patterns, typically because they can +// live under multiple parents. +// +// Example: +// +// message LogEntry { +// option (google.api.resource) = { +// type: "logging.googleapis.com/LogEntry" +// pattern: "projects/{project}/logs/{log}" +// pattern: "folders/{folder}/logs/{log}" +// pattern: "organizations/{organization}/logs/{log}" +// pattern: "billingAccounts/{billing_account}/logs/{log}" +// }; +// } +// +// The ResourceDescriptor Yaml config will look like: +// +// resources: +// - type: 'logging.googleapis.com/LogEntry' +// pattern: "projects/{project}/logs/{log}" +// pattern: "folders/{folder}/logs/{log}" +// pattern: "organizations/{organization}/logs/{log}" +// pattern: "billingAccounts/{billing_account}/logs/{log}" +message ResourceDescriptor { + // A description of the historical or future-looking state of the + // resource pattern. + enum History { + // The "unset" value. + HISTORY_UNSPECIFIED = 0; + + // The resource originally had one pattern and launched as such, and + // additional patterns were added later. + ORIGINALLY_SINGLE_PATTERN = 1; + + // The resource has one pattern, but the API owner expects to add more + // later. (This is the inverse of ORIGINALLY_SINGLE_PATTERN, and prevents + // that from being necessary once there are multiple patterns.) + FUTURE_MULTI_PATTERN = 2; + } + + // A flag representing a specific style that a resource claims to conform to. + enum Style { + // The unspecified value. Do not use. + STYLE_UNSPECIFIED = 0; + + // This resource is intended to be "declarative-friendly". + // + // Declarative-friendly resources must be more strictly consistent, and + // setting this to true communicates to tools that this resource should + // adhere to declarative-friendly expectations. + // + // Note: This is used by the API linter (linter.aip.dev) to enable + // additional checks. + DECLARATIVE_FRIENDLY = 1; + } + + // The resource type. It must be in the format of + // {service_name}/{resource_type_kind}. The `resource_type_kind` must be + // singular and must not include version numbers. + // + // Example: `storage.googleapis.com/Bucket` + // + // The value of the resource_type_kind must follow the regular expression + // /[A-Za-z][a-zA-Z0-9]+/. It should start with an upper case character and + // should use PascalCase (UpperCamelCase). The maximum number of + // characters allowed for the `resource_type_kind` is 100. + string type = 1; + + // Optional. The relative resource name pattern associated with this resource + // type. The DNS prefix of the full resource name shouldn't be specified here. + // + // The path pattern must follow the syntax, which aligns with HTTP binding + // syntax: + // + // Template = Segment { "/" Segment } ; + // Segment = LITERAL | Variable ; + // Variable = "{" LITERAL "}" ; + // + // Examples: + // + // - "projects/{project}/topics/{topic}" + // - "projects/{project}/knowledgeBases/{knowledge_base}" + // + // The components in braces correspond to the IDs for each resource in the + // hierarchy. It is expected that, if multiple patterns are provided, + // the same component name (e.g. "project") refers to IDs of the same + // type of resource. + repeated string pattern = 2; + + // Optional. The field on the resource that designates the resource name + // field. If omitted, this is assumed to be "name". + string name_field = 3; + + // Optional. The historical or future-looking state of the resource pattern. + // + // Example: + // + // // The InspectTemplate message originally only supported resource + // // names with organization, and project was added later. + // message InspectTemplate { + // option (google.api.resource) = { + // type: "dlp.googleapis.com/InspectTemplate" + // pattern: + // "organizations/{organization}/inspectTemplates/{inspect_template}" + // pattern: "projects/{project}/inspectTemplates/{inspect_template}" + // history: ORIGINALLY_SINGLE_PATTERN + // }; + // } + History history = 4; + + // The plural name used in the resource name and permission names, such as + // 'projects' for the resource name of 'projects/{project}' and the permission + // name of 'cloudresourcemanager.googleapis.com/projects.get'. It is the same + // concept of the `plural` field in k8s CRD spec + // https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ + // + // Note: The plural form is required even for singleton resources. See + // https://aip.dev/156 + string plural = 5; + + // The same concept of the `singular` field in k8s CRD spec + // https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ + // Such as "project" for the `resourcemanager.googleapis.com/Project` type. + string singular = 6; + + // Style flag(s) for this resource. + // These indicate that a resource is expected to conform to a given + // style. See the specific style flags for additional information. + repeated Style style = 10; +} + +// Defines a proto annotation that describes a string field that refers to +// an API resource. +message ResourceReference { + // The resource type that the annotated field references. + // + // Example: + // + // message Subscription { + // string topic = 2 [(google.api.resource_reference) = { + // type: "pubsub.googleapis.com/Topic" + // }]; + // } + // + // Occasionally, a field may reference an arbitrary resource. In this case, + // APIs use the special value * in their resource reference. + // + // Example: + // + // message GetIamPolicyRequest { + // string resource = 2 [(google.api.resource_reference) = { + // type: "*" + // }]; + // } + string type = 1; + + // The resource type of a child collection that the annotated field + // references. This is useful for annotating the `parent` field that + // doesn't have a fixed resource type. + // + // Example: + // + // message ListLogEntriesRequest { + // string parent = 1 [(google.api.resource_reference) = { + // child_type: "logging.googleapis.com/LogEntry" + // }; + // } + string child_type = 2; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/api/visibility.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/visibility.proto new file mode 100644 index 00000000..0be4581d --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/api/visibility.proto @@ -0,0 +1,113 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/visibility;visibility"; +option java_multiple_files = true; +option java_outer_classname = "VisibilityProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.EnumOptions { + // See `VisibilityRule`. + google.api.VisibilityRule enum_visibility = 72295727; +} + +extend google.protobuf.EnumValueOptions { + // See `VisibilityRule`. + google.api.VisibilityRule value_visibility = 72295727; +} + +extend google.protobuf.FieldOptions { + // See `VisibilityRule`. + google.api.VisibilityRule field_visibility = 72295727; +} + +extend google.protobuf.MessageOptions { + // See `VisibilityRule`. + google.api.VisibilityRule message_visibility = 72295727; +} + +extend google.protobuf.MethodOptions { + // See `VisibilityRule`. + google.api.VisibilityRule method_visibility = 72295727; +} + +extend google.protobuf.ServiceOptions { + // See `VisibilityRule`. + google.api.VisibilityRule api_visibility = 72295727; +} + +// `Visibility` restricts service consumer's access to service elements, +// such as whether an application can call a visibility-restricted method. +// The restriction is expressed by applying visibility labels on service +// elements. The visibility labels are elsewhere linked to service consumers. +// +// A service can define multiple visibility labels, but a service consumer +// should be granted at most one visibility label. Multiple visibility +// labels for a single service consumer are not supported. +// +// If an element and all its parents have no visibility label, its visibility +// is unconditionally granted. +// +// Example: +// +// visibility: +// rules: +// - selector: google.calendar.Calendar.EnhancedSearch +// restriction: PREVIEW +// - selector: google.calendar.Calendar.Delegate +// restriction: INTERNAL +// +// Here, all methods are publicly visible except for the restricted methods +// EnhancedSearch and Delegate. +message Visibility { + // A list of visibility rules that apply to individual API elements. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated VisibilityRule rules = 1; +} + +// A visibility rule provides visibility configuration for an individual API +// element. +message VisibilityRule { + // Selects methods, messages, fields, enums, etc. to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax + // details. + string selector = 1; + + // A comma-separated list of visibility labels that apply to the `selector`. + // Any of the listed labels can be used to grant the visibility. + // + // If a rule has multiple labels, removing one of the labels but not all of + // them can break clients. + // + // Example: + // + // visibility: + // rules: + // - selector: google.calendar.Calendar.EnhancedSearch + // restriction: INTERNAL, PREVIEW + // + // Removing INTERNAL from this restriction will break clients that rely on + // this method and only had access to it through INTERNAL. + string restriction = 2; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/any.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/any.proto new file mode 100644 index 00000000..eff44e50 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/any.proto @@ -0,0 +1,162 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option go_package = "google.golang.org/protobuf/types/known/anypb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "AnyProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; + +// `Any` contains an arbitrary serialized protocol buffer message along with a +// URL that describes the type of the serialized message. +// +// Protobuf library provides support to pack/unpack Any values in the form +// of utility functions or additional generated methods of the Any type. +// +// Example 1: Pack and unpack a message in C++. +// +// Foo foo = ...; +// Any any; +// any.PackFrom(foo); +// ... +// if (any.UnpackTo(&foo)) { +// ... +// } +// +// Example 2: Pack and unpack a message in Java. +// +// Foo foo = ...; +// Any any = Any.pack(foo); +// ... +// if (any.is(Foo.class)) { +// foo = any.unpack(Foo.class); +// } +// // or ... +// if (any.isSameTypeAs(Foo.getDefaultInstance())) { +// foo = any.unpack(Foo.getDefaultInstance()); +// } +// +// Example 3: Pack and unpack a message in Python. +// +// foo = Foo(...) +// any = Any() +// any.Pack(foo) +// ... +// if any.Is(Foo.DESCRIPTOR): +// any.Unpack(foo) +// ... +// +// Example 4: Pack and unpack a message in Go +// +// foo := &pb.Foo{...} +// any, err := anypb.New(foo) +// if err != nil { +// ... +// } +// ... +// foo := &pb.Foo{} +// if err := any.UnmarshalTo(foo); err != nil { +// ... +// } +// +// The pack methods provided by protobuf library will by default use +// 'type.googleapis.com/full.type.name' as the type URL and the unpack +// methods only use the fully qualified type name after the last '/' +// in the type URL, for example "foo.bar.com/x/y.z" will yield type +// name "y.z". +// +// JSON +// ==== +// The JSON representation of an `Any` value uses the regular +// representation of the deserialized, embedded message, with an +// additional field `@type` which contains the type URL. Example: +// +// package google.profile; +// message Person { +// string first_name = 1; +// string last_name = 2; +// } +// +// { +// "@type": "type.googleapis.com/google.profile.Person", +// "firstName": , +// "lastName": +// } +// +// If the embedded message type is well-known and has a custom JSON +// representation, that representation will be embedded adding a field +// `value` which holds the custom JSON in addition to the `@type` +// field. Example (for message [google.protobuf.Duration][]): +// +// { +// "@type": "type.googleapis.com/google.protobuf.Duration", +// "value": "1.212s" +// } +// +message Any { + // A URL/resource name that uniquely identifies the type of the serialized + // protocol buffer message. This string must contain at least + // one "/" character. The last segment of the URL's path must represent + // the fully qualified name of the type (as in + // `path/google.protobuf.Duration`). The name should be in a canonical form + // (e.g., leading "." is not accepted). + // + // In practice, teams usually precompile into the binary all types that they + // expect it to use in the context of Any. However, for URLs which use the + // scheme `http`, `https`, or no scheme, one can optionally set up a type + // server that maps type URLs to message definitions as follows: + // + // * If no scheme is provided, `https` is assumed. + // * An HTTP GET on the URL must yield a [google.protobuf.Type][] + // value in binary format, or produce an error. + // * Applications are allowed to cache lookup results based on the + // URL, or have them precompiled into a binary to avoid any + // lookup. Therefore, binary compatibility needs to be preserved + // on changes to types. (Use versioned type names to manage + // breaking changes.) + // + // Note: this functionality is not currently available in the official + // protobuf release, and it is not used for type URLs beginning with + // type.googleapis.com. As of May 2023, there are no widely used type server + // implementations and no plans to implement one. + // + // Schemes other than `http`, `https` (or the empty scheme) might be + // used with implementation specific semantics. + // + string type_url = 1; + + // Must be a valid serialized protocol buffer of the above specified type. + bytes value = 2; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/api.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/api.proto new file mode 100644 index 00000000..42223516 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/api.proto @@ -0,0 +1,207 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +import "google/protobuf/source_context.proto"; +import "google/protobuf/type.proto"; + +option java_package = "com.google.protobuf"; +option java_outer_classname = "ApiProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option go_package = "google.golang.org/protobuf/types/known/apipb"; + +// Api is a light-weight descriptor for an API Interface. +// +// Interfaces are also described as "protocol buffer services" in some contexts, +// such as by the "service" keyword in a .proto file, but they are different +// from API Services, which represent a concrete implementation of an interface +// as opposed to simply a description of methods and bindings. They are also +// sometimes simply referred to as "APIs" in other contexts, such as the name of +// this message itself. See https://cloud.google.com/apis/design/glossary for +// detailed terminology. +message Api { + // The fully qualified name of this interface, including package name + // followed by the interface's simple name. + string name = 1; + + // The methods of this interface, in unspecified order. + repeated Method methods = 2; + + // Any metadata attached to the interface. + repeated Option options = 3; + + // A version string for this interface. If specified, must have the form + // `major-version.minor-version`, as in `1.10`. If the minor version is + // omitted, it defaults to zero. If the entire version field is empty, the + // major version is derived from the package name, as outlined below. If the + // field is not empty, the version in the package name will be verified to be + // consistent with what is provided here. + // + // The versioning schema uses [semantic + // versioning](http://semver.org) where the major version number + // indicates a breaking change and the minor version an additive, + // non-breaking change. Both version numbers are signals to users + // what to expect from different versions, and should be carefully + // chosen based on the product plan. + // + // The major version is also reflected in the package name of the + // interface, which must end in `v`, as in + // `google.feature.v1`. For major versions 0 and 1, the suffix can + // be omitted. Zero major versions must only be used for + // experimental, non-GA interfaces. + // + string version = 4; + + // Source context for the protocol buffer service represented by this + // message. + SourceContext source_context = 5; + + // Included interfaces. See [Mixin][]. + repeated Mixin mixins = 6; + + // The source syntax of the service. + Syntax syntax = 7; +} + +// Method represents a method of an API interface. +message Method { + // The simple name of this method. + string name = 1; + + // A URL of the input message type. + string request_type_url = 2; + + // If true, the request is streamed. + bool request_streaming = 3; + + // The URL of the output message type. + string response_type_url = 4; + + // If true, the response is streamed. + bool response_streaming = 5; + + // Any metadata attached to the method. + repeated Option options = 6; + + // The source syntax of this method. + Syntax syntax = 7; +} + +// Declares an API Interface to be included in this interface. The including +// interface must redeclare all the methods from the included interface, but +// documentation and options are inherited as follows: +// +// - If after comment and whitespace stripping, the documentation +// string of the redeclared method is empty, it will be inherited +// from the original method. +// +// - Each annotation belonging to the service config (http, +// visibility) which is not set in the redeclared method will be +// inherited. +// +// - If an http annotation is inherited, the path pattern will be +// modified as follows. Any version prefix will be replaced by the +// version of the including interface plus the [root][] path if +// specified. +// +// Example of a simple mixin: +// +// package google.acl.v1; +// service AccessControl { +// // Get the underlying ACL object. +// rpc GetAcl(GetAclRequest) returns (Acl) { +// option (google.api.http).get = "/v1/{resource=**}:getAcl"; +// } +// } +// +// package google.storage.v2; +// service Storage { +// rpc GetAcl(GetAclRequest) returns (Acl); +// +// // Get a data record. +// rpc GetData(GetDataRequest) returns (Data) { +// option (google.api.http).get = "/v2/{resource=**}"; +// } +// } +// +// Example of a mixin configuration: +// +// apis: +// - name: google.storage.v2.Storage +// mixins: +// - name: google.acl.v1.AccessControl +// +// The mixin construct implies that all methods in `AccessControl` are +// also declared with same name and request/response types in +// `Storage`. A documentation generator or annotation processor will +// see the effective `Storage.GetAcl` method after inherting +// documentation and annotations as follows: +// +// service Storage { +// // Get the underlying ACL object. +// rpc GetAcl(GetAclRequest) returns (Acl) { +// option (google.api.http).get = "/v2/{resource=**}:getAcl"; +// } +// ... +// } +// +// Note how the version in the path pattern changed from `v1` to `v2`. +// +// If the `root` field in the mixin is specified, it should be a +// relative path under which inherited HTTP paths are placed. Example: +// +// apis: +// - name: google.storage.v2.Storage +// mixins: +// - name: google.acl.v1.AccessControl +// root: acls +// +// This implies the following inherited HTTP annotation: +// +// service Storage { +// // Get the underlying ACL object. +// rpc GetAcl(GetAclRequest) returns (Acl) { +// option (google.api.http).get = "/v2/acls/{resource=**}:getAcl"; +// } +// ... +// } +message Mixin { + // The fully qualified name of the interface which is included. + string name = 1; + + // If non-empty specifies a path under which inherited HTTP paths + // are rooted. + string root = 2; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/descriptor.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/descriptor.proto new file mode 100644 index 00000000..47486435 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/descriptor.proto @@ -0,0 +1,1218 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: kenton@google.com (Kenton Varda) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// The messages in this file describe the definitions found in .proto files. +// A valid .proto file can be translated directly to a FileDescriptorProto +// without any other information (e.g. without reading its imports). + +syntax = "proto2"; + +package google.protobuf; + +option go_package = "google.golang.org/protobuf/types/descriptorpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DescriptorProtos"; +option csharp_namespace = "Google.Protobuf.Reflection"; +option objc_class_prefix = "GPB"; +option cc_enable_arenas = true; + +// descriptor.proto must be optimized for speed because reflection-based +// algorithms don't work during bootstrapping. +option optimize_for = SPEED; + +// The protocol compiler can output a FileDescriptorSet containing the .proto +// files it parses. +message FileDescriptorSet { + repeated FileDescriptorProto file = 1; +} + +// The full set of known editions. +enum Edition { + // A placeholder for an unknown edition value. + EDITION_UNKNOWN = 0; + + // Legacy syntax "editions". These pre-date editions, but behave much like + // distinct editions. These can't be used to specify the edition of proto + // files, but feature definitions must supply proto2/proto3 defaults for + // backwards compatibility. + EDITION_PROTO2 = 998; + EDITION_PROTO3 = 999; + + // Editions that have been released. The specific values are arbitrary and + // should not be depended on, but they will always be time-ordered for easy + // comparison. + EDITION_2023 = 1000; + + // Placeholder editions for testing feature resolution. These should not be + // used or relyed on outside of tests. + EDITION_1_TEST_ONLY = 1; + EDITION_2_TEST_ONLY = 2; + EDITION_99997_TEST_ONLY = 99997; + EDITION_99998_TEST_ONLY = 99998; + EDITION_99999_TEST_ONLY = 99999; +} + +// Describes a complete .proto file. +message FileDescriptorProto { + optional string name = 1; // file name, relative to root of source tree + optional string package = 2; // e.g. "foo", "foo.bar", etc. + + // Names of files imported by this file. + repeated string dependency = 3; + // Indexes of the public imported files in the dependency list above. + repeated int32 public_dependency = 10; + // Indexes of the weak imported files in the dependency list. + // For Google-internal migration only. Do not use. + repeated int32 weak_dependency = 11; + + // All top-level definitions in this file. + repeated DescriptorProto message_type = 4; + repeated EnumDescriptorProto enum_type = 5; + repeated ServiceDescriptorProto service = 6; + repeated FieldDescriptorProto extension = 7; + + optional FileOptions options = 8; + + // This field contains optional information about the original source code. + // You may safely remove this entire field without harming runtime + // functionality of the descriptors -- the information is needed only by + // development tools. + optional SourceCodeInfo source_code_info = 9; + + // The syntax of the proto file. + // The supported values are "proto2", "proto3", and "editions". + // + // If `edition` is present, this value must be "editions". + optional string syntax = 12; + + // The edition of the proto file. + optional Edition edition = 14; +} + +// Describes a message type. +message DescriptorProto { + optional string name = 1; + + repeated FieldDescriptorProto field = 2; + repeated FieldDescriptorProto extension = 6; + + repeated DescriptorProto nested_type = 3; + repeated EnumDescriptorProto enum_type = 4; + + message ExtensionRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + + optional ExtensionRangeOptions options = 3; + } + repeated ExtensionRange extension_range = 5; + + repeated OneofDescriptorProto oneof_decl = 8; + + optional MessageOptions options = 7; + + // Range of reserved tag numbers. Reserved tag numbers may not be used by + // fields or extension ranges in the same message. Reserved ranges may + // not overlap. + message ReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + } + repeated ReservedRange reserved_range = 9; + // Reserved field names, which may not be used by fields in the same message. + // A given name may only be reserved once. + repeated string reserved_name = 10; +} + +message ExtensionRangeOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + message Declaration { + // The extension number declared within the extension range. + optional int32 number = 1; + + // The fully-qualified name of the extension field. There must be a leading + // dot in front of the full name. + optional string full_name = 2; + + // The fully-qualified type name of the extension field. Unlike + // Metadata.type, Declaration.type must have a leading dot for messages + // and enums. + optional string type = 3; + + // If true, indicates that the number is reserved in the extension range, + // and any extension field with the number will fail to compile. Set this + // when a declared extension field is deleted. + optional bool reserved = 5; + + // If true, indicates that the extension must be defined as repeated. + // Otherwise the extension must be defined as optional. + optional bool repeated = 6; + + reserved 4; // removed is_repeated + } + + // For external users: DO NOT USE. We are in the process of open sourcing + // extension declaration and executing internal cleanups before it can be + // used externally. + repeated Declaration declaration = 2 [retention = RETENTION_SOURCE]; + + // Any features defined in the specific edition. + optional FeatureSet features = 50; + + // The verification state of the extension range. + enum VerificationState { + // All the extensions of the range must be declared. + DECLARATION = 0; + UNVERIFIED = 1; + } + + // The verification state of the range. + // TODO: flip the default to DECLARATION once all empty ranges + // are marked as UNVERIFIED. + optional VerificationState verification = 3 [default = UNVERIFIED]; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// Describes a field within a message. +message FieldDescriptorProto { + enum Type { + // 0 is reserved for errors. + // Order is weird for historical reasons. + TYPE_DOUBLE = 1; + TYPE_FLOAT = 2; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if + // negative values are likely. + TYPE_INT64 = 3; + TYPE_UINT64 = 4; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if + // negative values are likely. + TYPE_INT32 = 5; + TYPE_FIXED64 = 6; + TYPE_FIXED32 = 7; + TYPE_BOOL = 8; + TYPE_STRING = 9; + // Tag-delimited aggregate. + // Group type is deprecated and not supported after google.protobuf. However, Proto3 + // implementations should still be able to parse the group wire format and + // treat group fields as unknown fields. In Editions, the group wire format + // can be enabled via the `message_encoding` feature. + TYPE_GROUP = 10; + TYPE_MESSAGE = 11; // Length-delimited aggregate. + + // New in version 2. + TYPE_BYTES = 12; + TYPE_UINT32 = 13; + TYPE_ENUM = 14; + TYPE_SFIXED32 = 15; + TYPE_SFIXED64 = 16; + TYPE_SINT32 = 17; // Uses ZigZag encoding. + TYPE_SINT64 = 18; // Uses ZigZag encoding. + } + + enum Label { + // 0 is reserved for errors + LABEL_OPTIONAL = 1; + LABEL_REPEATED = 3; + // The required label is only allowed in google.protobuf. In proto3 and Editions + // it's explicitly prohibited. In Editions, the `field_presence` feature + // can be used to get this behavior. + LABEL_REQUIRED = 2; + } + + optional string name = 1; + optional int32 number = 3; + optional Label label = 4; + + // If type_name is set, this need not be set. If both this and type_name + // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. + optional Type type = 5; + + // For message and enum types, this is the name of the type. If the name + // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping + // rules are used to find the type (i.e. first the nested types within this + // message are searched, then within the parent, on up to the root + // namespace). + optional string type_name = 6; + + // For extensions, this is the name of the type being extended. It is + // resolved in the same manner as type_name. + optional string extendee = 2; + + // For numeric types, contains the original text representation of the value. + // For booleans, "true" or "false". + // For strings, contains the default text contents (not escaped in any way). + // For bytes, contains the C escaped value. All bytes >= 128 are escaped. + optional string default_value = 7; + + // If set, gives the index of a oneof in the containing type's oneof_decl + // list. This field is a member of that oneof. + optional int32 oneof_index = 9; + + // JSON name of this field. The value is set by protocol compiler. If the + // user has set a "json_name" option on this field, that option's value + // will be used. Otherwise, it's deduced from the field's name by converting + // it to camelCase. + optional string json_name = 10; + + optional FieldOptions options = 8; + + // If true, this is a proto3 "optional". When a proto3 field is optional, it + // tracks presence regardless of field type. + // + // When proto3_optional is true, this field must be belong to a oneof to + // signal to old proto3 clients that presence is tracked for this field. This + // oneof is known as a "synthetic" oneof, and this field must be its sole + // member (each proto3 optional field gets its own synthetic oneof). Synthetic + // oneofs exist in the descriptor only, and do not generate any API. Synthetic + // oneofs must be ordered after all "real" oneofs. + // + // For message fields, proto3_optional doesn't create any semantic change, + // since non-repeated message fields always track presence. However it still + // indicates the semantic detail of whether the user wrote "optional" or not. + // This can be useful for round-tripping the .proto file. For consistency we + // give message fields a synthetic oneof also, even though it is not required + // to track presence. This is especially important because the parser can't + // tell if a field is a message or an enum, so it must always create a + // synthetic oneof. + // + // Proto2 optional fields do not set this flag, because they already indicate + // optional with `LABEL_OPTIONAL`. + optional bool proto3_optional = 17; +} + +// Describes a oneof. +message OneofDescriptorProto { + optional string name = 1; + optional OneofOptions options = 2; +} + +// Describes an enum type. +message EnumDescriptorProto { + optional string name = 1; + + repeated EnumValueDescriptorProto value = 2; + + optional EnumOptions options = 3; + + // Range of reserved numeric values. Reserved values may not be used by + // entries in the same enum. Reserved ranges may not overlap. + // + // Note that this is distinct from DescriptorProto.ReservedRange in that it + // is inclusive such that it can appropriately represent the entire int32 + // domain. + message EnumReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Inclusive. + } + + // Range of reserved numeric values. Reserved numeric values may not be used + // by enum values in the same enum declaration. Reserved ranges may not + // overlap. + repeated EnumReservedRange reserved_range = 4; + + // Reserved enum value names, which may not be reused. A given name may only + // be reserved once. + repeated string reserved_name = 5; +} + +// Describes a value within an enum. +message EnumValueDescriptorProto { + optional string name = 1; + optional int32 number = 2; + + optional EnumValueOptions options = 3; +} + +// Describes a service. +message ServiceDescriptorProto { + optional string name = 1; + repeated MethodDescriptorProto method = 2; + + optional ServiceOptions options = 3; +} + +// Describes a method of a service. +message MethodDescriptorProto { + optional string name = 1; + + // Input and output type names. These are resolved in the same way as + // FieldDescriptorProto.type_name, but must refer to a message type. + optional string input_type = 2; + optional string output_type = 3; + + optional MethodOptions options = 4; + + // Identifies if client streams multiple client messages + optional bool client_streaming = 5 [default = false]; + // Identifies if server streams multiple server messages + optional bool server_streaming = 6 [default = false]; +} + +// =================================================================== +// Options + +// Each of the definitions above may have "options" attached. These are +// just annotations which may cause code to be generated slightly differently +// or may contain hints for code that manipulates protocol messages. +// +// Clients may define custom options as extensions of the *Options messages. +// These extensions may not yet be known at parsing time, so the parser cannot +// store the values in them. Instead it stores them in a field in the *Options +// message called uninterpreted_option. This field must have the same name +// across all *Options messages. We then use this field to populate the +// extensions when we build a descriptor, at which point all protos have been +// parsed and so all extensions are known. +// +// Extension numbers for custom options may be chosen as follows: +// * For options which will only be used within a single application or +// organization, or for experimental options, use field numbers 50000 +// through 99999. It is up to you to ensure that you do not use the +// same number for multiple options. +// * For options which will be published and used publicly by multiple +// independent entities, e-mail protobuf-global-extension-registry@google.com +// to reserve extension numbers. Simply provide your project name (e.g. +// Objective-C plugin) and your project website (if available) -- there's no +// need to explain how you intend to use them. Usually you only need one +// extension number. You can declare multiple options with only one extension +// number by putting them in a sub-message. See the Custom Options section of +// the docs for examples: +// https://developers.google.com/protocol-buffers/docs/proto#options +// If this turns out to be popular, a web service will be set up +// to automatically assign option numbers. + +message FileOptions { + + // Sets the Java package where classes generated from this .proto will be + // placed. By default, the proto package is used, but this is often + // inappropriate because proto packages do not normally start with backwards + // domain names. + optional string java_package = 1; + + // Controls the name of the wrapper Java class generated for the .proto file. + // That class will always contain the .proto file's getDescriptor() method as + // well as any top-level extensions defined in the .proto file. + // If java_multiple_files is disabled, then all the other classes from the + // .proto file will be nested inside the single wrapper outer class. + optional string java_outer_classname = 8; + + // If enabled, then the Java code generator will generate a separate .java + // file for each top-level message, enum, and service defined in the .proto + // file. Thus, these types will *not* be nested inside the wrapper class + // named by java_outer_classname. However, the wrapper class will still be + // generated to contain the file's getDescriptor() method as well as any + // top-level extensions defined in the file. + optional bool java_multiple_files = 10 [default = false]; + + // This option does nothing. + optional bool java_generate_equals_and_hash = 20 [deprecated=true]; + + // If set true, then the Java2 code generator will generate code that + // throws an exception whenever an attempt is made to assign a non-UTF-8 + // byte sequence to a string field. + // Message reflection will do the same. + // However, an extension field still accepts non-UTF-8 byte sequences. + // This option has no effect on when used with the lite runtime. + optional bool java_string_check_utf8 = 27 [default = false]; + + // Generated classes can be optimized for speed or code size. + enum OptimizeMode { + SPEED = 1; // Generate complete code for parsing, serialization, + // etc. + CODE_SIZE = 2; // Use ReflectionOps to implement these methods. + LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. + } + optional OptimizeMode optimize_for = 9 [default = SPEED]; + + // Sets the Go package where structs generated from this .proto will be + // placed. If omitted, the Go package will be derived from the following: + // - The basename of the package import path, if provided. + // - Otherwise, the package statement in the .proto file, if present. + // - Otherwise, the basename of the .proto file, without extension. + optional string go_package = 11; + + // Should generic services be generated in each language? "Generic" services + // are not specific to any particular RPC system. They are generated by the + // main code generators in each language (without additional plugins). + // Generic services were the only kind of service generation supported by + // early versions of google.protobuf. + // + // Generic services are now considered deprecated in favor of using plugins + // that generate code specific to your particular RPC system. Therefore, + // these default to false. Old code which depends on generic services should + // explicitly set them to true. + optional bool cc_generic_services = 16 [default = false]; + optional bool java_generic_services = 17 [default = false]; + optional bool py_generic_services = 18 [default = false]; + optional bool php_generic_services = 42 [default = false]; + + // Is this file deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for everything in the file, or it will be completely ignored; in the very + // least, this is a formalization for deprecating files. + optional bool deprecated = 23 [default = false]; + + // Enables the use of arenas for the proto messages in this file. This applies + // only to generated classes for C++. + optional bool cc_enable_arenas = 31 [default = true]; + + // Sets the objective c class prefix which is prepended to all objective c + // generated classes from this .proto. There is no default. + optional string objc_class_prefix = 36; + + // Namespace for generated classes; defaults to the package. + optional string csharp_namespace = 37; + + // By default Swift generators will take the proto package and CamelCase it + // replacing '.' with underscore and use that to prefix the types/symbols + // defined. When this options is provided, they will use this value instead + // to prefix the types/symbols defined. + optional string swift_prefix = 39; + + // Sets the php class prefix which is prepended to all php generated classes + // from this .proto. Default is empty. + optional string php_class_prefix = 40; + + // Use this option to change the namespace of php generated classes. Default + // is empty. When this option is empty, the package name will be used for + // determining the namespace. + optional string php_namespace = 41; + + // Use this option to change the namespace of php generated metadata classes. + // Default is empty. When this option is empty, the proto file name will be + // used for determining the namespace. + optional string php_metadata_namespace = 44; + + // Use this option to change the package of ruby generated classes. Default + // is empty. When this option is not set, the package name will be used for + // determining the ruby package. + optional string ruby_package = 45; + + // Any features defined in the specific edition. + optional FeatureSet features = 50; + + // The parser stores options it doesn't recognize here. + // See the documentation for the "Options" section above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. + // See the documentation for the "Options" section above. + extensions 1000 to max; + + reserved 38; +} + +message MessageOptions { + // Set true to use the old proto1 MessageSet wire format for extensions. + // This is provided for backwards-compatibility with the MessageSet wire + // format. You should not use this for any other reason: It's less + // efficient, has fewer features, and is more complicated. + // + // The message must be defined exactly as follows: + // message Foo { + // option message_set_wire_format = true; + // extensions 4 to max; + // } + // Note that the message cannot have any defined fields; MessageSets only + // have extensions. + // + // All extensions of your type must be singular messages; e.g. they cannot + // be int32s, enums, or repeated messages. + // + // Because this is an option, the above two restrictions are not enforced by + // the protocol compiler. + optional bool message_set_wire_format = 1 [default = false]; + + // Disables the generation of the standard "descriptor()" accessor, which can + // conflict with a field of the same name. This is meant to make migration + // from proto1 easier; new code should avoid fields named "descriptor". + optional bool no_standard_descriptor_accessor = 2 [default = false]; + + // Is this message deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the message, or it will be completely ignored; in the very least, + // this is a formalization for deprecating messages. + optional bool deprecated = 3 [default = false]; + + reserved 4, 5, 6; + + // NOTE: Do not set the option in .proto files. Always use the maps syntax + // instead. The option should only be implicitly set by the proto compiler + // parser. + // + // Whether the message is an automatically generated map entry type for the + // maps field. + // + // For maps fields: + // map map_field = 1; + // The parsed descriptor looks like: + // message MapFieldEntry { + // option map_entry = true; + // optional KeyType key = 1; + // optional ValueType value = 2; + // } + // repeated MapFieldEntry map_field = 1; + // + // Implementations may choose not to generate the map_entry=true message, but + // use a native map in the target language to hold the keys and values. + // The reflection APIs in such implementations still need to work as + // if the field is a repeated message field. + optional bool map_entry = 7; + + reserved 8; // javalite_serializable + reserved 9; // javanano_as_lite + + // Enable the legacy handling of JSON field name conflicts. This lowercases + // and strips underscored from the fields before comparison in proto3 only. + // The new behavior takes `json_name` into account and applies to proto2 as + // well. + // + // This should only be used as a temporary measure against broken builds due + // to the change in behavior for JSON field name conflicts. + // + // TODO This is legacy behavior we plan to remove once downstream + // teams have had time to migrate. + optional bool deprecated_legacy_json_field_conflicts = 11 [deprecated = true]; + + // Any features defined in the specific edition. + optional FeatureSet features = 12; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message FieldOptions { + // The ctype option instructs the C++ code generator to use a different + // representation of the field than it normally would. See the specific + // options below. This option is only implemented to support use of + // [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of + // type "bytes" in the open source release -- sorry, we'll try to include + // other types in a future version! + optional CType ctype = 1 [default = STRING]; + enum CType { + // Default mode. + STRING = 0; + + // The option [ctype=CORD] may be applied to a non-repeated field of type + // "bytes". It indicates that in C++, the data should be stored in a Cord + // instead of a string. For very large strings, this may reduce memory + // fragmentation. It may also allow better performance when parsing from a + // Cord, or when parsing with aliasing enabled, as the parsed Cord may then + // alias the original buffer. + CORD = 1; + + STRING_PIECE = 2; + } + // The packed option can be enabled for repeated primitive fields to enable + // a more efficient representation on the wire. Rather than repeatedly + // writing the tag and type for each element, the entire array is encoded as + // a single length-delimited blob. In proto3, only explicit setting it to + // false will avoid using packed encoding. This option is prohibited in + // Editions, but the `repeated_field_encoding` feature can be used to control + // the behavior. + optional bool packed = 2; + + // The jstype option determines the JavaScript type used for values of the + // field. The option is permitted only for 64 bit integral and fixed types + // (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING + // is represented as JavaScript string, which avoids loss of precision that + // can happen when a large value is converted to a floating point JavaScript. + // Specifying JS_NUMBER for the jstype causes the generated JavaScript code to + // use the JavaScript "number" type. The behavior of the default option + // JS_NORMAL is implementation dependent. + // + // This option is an enum to permit additional types to be added, e.g. + // goog.math.Integer. + optional JSType jstype = 6 [default = JS_NORMAL]; + enum JSType { + // Use the default type. + JS_NORMAL = 0; + + // Use JavaScript strings. + JS_STRING = 1; + + // Use JavaScript numbers. + JS_NUMBER = 2; + } + + // Should this field be parsed lazily? Lazy applies only to message-type + // fields. It means that when the outer message is initially parsed, the + // inner message's contents will not be parsed but instead stored in encoded + // form. The inner message will actually be parsed when it is first accessed. + // + // This is only a hint. Implementations are free to choose whether to use + // eager or lazy parsing regardless of the value of this option. However, + // setting this option true suggests that the protocol author believes that + // using lazy parsing on this field is worth the additional bookkeeping + // overhead typically needed to implement it. + // + // This option does not affect the public interface of any generated code; + // all method signatures remain the same. Furthermore, thread-safety of the + // interface is not affected by this option; const methods remain safe to + // call from multiple threads concurrently, while non-const methods continue + // to require exclusive access. + // + // Note that implementations may choose not to check required fields within + // a lazy sub-message. That is, calling IsInitialized() on the outer message + // may return true even if the inner message has missing required fields. + // This is necessary because otherwise the inner message would have to be + // parsed in order to perform the check, defeating the purpose of lazy + // parsing. An implementation which chooses not to check required fields + // must be consistent about it. That is, for any particular sub-message, the + // implementation must either *always* check its required fields, or *never* + // check its required fields, regardless of whether or not the message has + // been parsed. + // + // As of May 2022, lazy verifies the contents of the byte stream during + // parsing. An invalid byte stream will cause the overall parsing to fail. + optional bool lazy = 5 [default = false]; + + // unverified_lazy does no correctness checks on the byte stream. This should + // only be used where lazy with verification is prohibitive for performance + // reasons. + optional bool unverified_lazy = 15 [default = false]; + + // Is this field deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for accessors, or it will be completely ignored; in the very least, this + // is a formalization for deprecating fields. + optional bool deprecated = 3 [default = false]; + + // For Google-internal migration only. Do not use. + optional bool weak = 10 [default = false]; + + // Indicate that the field value should not be printed out when using debug + // formats, e.g. when the field contains sensitive credentials. + optional bool debug_redact = 16 [default = false]; + + // If set to RETENTION_SOURCE, the option will be omitted from the binary. + // Note: as of January 2023, support for this is in progress and does not yet + // have an effect (b/264593489). + enum OptionRetention { + RETENTION_UNKNOWN = 0; + RETENTION_RUNTIME = 1; + RETENTION_SOURCE = 2; + } + + optional OptionRetention retention = 17; + + // This indicates the types of entities that the field may apply to when used + // as an option. If it is unset, then the field may be freely used as an + // option on any kind of entity. Note: as of January 2023, support for this is + // in progress and does not yet have an effect (b/264593489). + enum OptionTargetType { + TARGET_TYPE_UNKNOWN = 0; + TARGET_TYPE_FILE = 1; + TARGET_TYPE_EXTENSION_RANGE = 2; + TARGET_TYPE_MESSAGE = 3; + TARGET_TYPE_FIELD = 4; + TARGET_TYPE_ONEOF = 5; + TARGET_TYPE_ENUM = 6; + TARGET_TYPE_ENUM_ENTRY = 7; + TARGET_TYPE_SERVICE = 8; + TARGET_TYPE_METHOD = 9; + } + + repeated OptionTargetType targets = 19; + + message EditionDefault { + optional Edition edition = 3; + optional string value = 2; // Textproto value. + } + repeated EditionDefault edition_defaults = 20; + + // Any features defined in the specific edition. + optional FeatureSet features = 21; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; + + reserved 4; // removed jtype + reserved 18; // reserve target, target_obsolete_do_not_use +} + +message OneofOptions { + // Any features defined in the specific edition. + optional FeatureSet features = 1; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumOptions { + + // Set this option to true to allow mapping different tag names to the same + // value. + optional bool allow_alias = 2; + + // Is this enum deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum, or it will be completely ignored; in the very least, this + // is a formalization for deprecating enums. + optional bool deprecated = 3 [default = false]; + + reserved 5; // javanano_as_lite + + // Enable the legacy handling of JSON field name conflicts. This lowercases + // and strips underscored from the fields before comparison in proto3 only. + // The new behavior takes `json_name` into account and applies to proto2 as + // well. + // TODO Remove this legacy behavior once downstream teams have + // had time to migrate. + optional bool deprecated_legacy_json_field_conflicts = 6 [deprecated = true]; + + // Any features defined in the specific edition. + optional FeatureSet features = 7; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumValueOptions { + // Is this enum value deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum value, or it will be completely ignored; in the very least, + // this is a formalization for deprecating enum values. + optional bool deprecated = 1 [default = false]; + + // Any features defined in the specific edition. + optional FeatureSet features = 2; + + // Indicate that fields annotated with this enum value should not be printed + // out when using debug formats, e.g. when the field contains sensitive + // credentials. + optional bool debug_redact = 3 [default = false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message ServiceOptions { + + // Any features defined in the specific edition. + optional FeatureSet features = 34; + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this service deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the service, or it will be completely ignored; in the very least, + // this is a formalization for deprecating services. + optional bool deprecated = 33 [default = false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message MethodOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this method deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the method, or it will be completely ignored; in the very least, + // this is a formalization for deprecating methods. + optional bool deprecated = 33 [default = false]; + + // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, + // or neither? HTTP based RPC implementation may choose GET verb for safe + // methods, and PUT verb for idempotent methods instead of the default POST. + enum IdempotencyLevel { + IDEMPOTENCY_UNKNOWN = 0; + NO_SIDE_EFFECTS = 1; // implies idempotent + IDEMPOTENT = 2; // idempotent, but may have side effects + } + optional IdempotencyLevel idempotency_level = 34 + [default = IDEMPOTENCY_UNKNOWN]; + + // Any features defined in the specific edition. + optional FeatureSet features = 35; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// A message representing a option the parser does not recognize. This only +// appears in options protos created by the compiler::Parser class. +// DescriptorPool resolves these when building Descriptor objects. Therefore, +// options protos in descriptor objects (e.g. returned by Descriptor::options(), +// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions +// in them. +message UninterpretedOption { + // The name of the uninterpreted option. Each string represents a segment in + // a dot-separated name. is_extension is true iff a segment represents an + // extension (denoted with parentheses in options specs in .proto files). + // E.g.,{ ["foo", false], ["bar.baz", true], ["moo", false] } represents + // "foo.(bar.baz).moo". + message NamePart { + required string name_part = 1; + required bool is_extension = 2; + } + repeated NamePart name = 2; + + // The value of the uninterpreted option, in whatever type the tokenizer + // identified it as during parsing. Exactly one of these should be set. + optional string identifier_value = 3; + optional uint64 positive_int_value = 4; + optional int64 negative_int_value = 5; + optional double double_value = 6; + optional bytes string_value = 7; + optional string aggregate_value = 8; +} + +// =================================================================== +// Features + +// TODO Enums in C++ gencode (and potentially other languages) are +// not well scoped. This means that each of the feature enums below can clash +// with each other. The short names we've chosen maximize call-site +// readability, but leave us very open to this scenario. A future feature will +// be designed and implemented to handle this, hopefully before we ever hit a +// conflict here. +message FeatureSet { + enum FieldPresence { + FIELD_PRESENCE_UNKNOWN = 0; + EXPLICIT = 1; + IMPLICIT = 2; + LEGACY_REQUIRED = 3; + } + optional FieldPresence field_presence = 1 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "EXPLICIT" }, + edition_defaults = { edition: EDITION_PROTO3, value: "IMPLICIT" }, + edition_defaults = { edition: EDITION_2023, value: "EXPLICIT" } + ]; + + enum EnumType { + ENUM_TYPE_UNKNOWN = 0; + OPEN = 1; + CLOSED = 2; + } + optional EnumType enum_type = 2 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_ENUM, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "CLOSED" }, + edition_defaults = { edition: EDITION_PROTO3, value: "OPEN" } + ]; + + enum RepeatedFieldEncoding { + REPEATED_FIELD_ENCODING_UNKNOWN = 0; + PACKED = 1; + EXPANDED = 2; + } + optional RepeatedFieldEncoding repeated_field_encoding = 3 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "EXPANDED" }, + edition_defaults = { edition: EDITION_PROTO3, value: "PACKED" } + ]; + + enum Utf8Validation { + UTF8_VALIDATION_UNKNOWN = 0; + NONE = 1; + VERIFY = 2; + } + optional Utf8Validation utf8_validation = 4 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "NONE" }, + edition_defaults = { edition: EDITION_PROTO3, value: "VERIFY" } + ]; + + enum MessageEncoding { + MESSAGE_ENCODING_UNKNOWN = 0; + LENGTH_PREFIXED = 1; + DELIMITED = 2; + } + optional MessageEncoding message_encoding = 5 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "LENGTH_PREFIXED" } + ]; + + enum JsonFormat { + JSON_FORMAT_UNKNOWN = 0; + ALLOW = 1; + LEGACY_BEST_EFFORT = 2; + } + optional JsonFormat json_format = 6 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_MESSAGE, + targets = TARGET_TYPE_ENUM, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "LEGACY_BEST_EFFORT" }, + edition_defaults = { edition: EDITION_PROTO3, value: "ALLOW" } + ]; + + reserved 999; + + extensions 1000; // for Protobuf C++ + extensions 1001; // for Protobuf Java + + extensions 9995 to 9999; // For internal testing +} + +// A compiled specification for the defaults of a set of features. These +// messages are generated from FeatureSet extensions and can be used to seed +// feature resolution. The resolution with this object becomes a simple search +// for the closest matching edition, followed by proto merges. +message FeatureSetDefaults { + // A map from every known edition with a unique set of defaults to its + // defaults. Not all editions may be contained here. For a given edition, + // the defaults at the closest matching edition ordered at or before it should + // be used. This field must be in strict ascending order by edition. + message FeatureSetEditionDefault { + optional Edition edition = 3; + optional FeatureSet features = 2; + } + repeated FeatureSetEditionDefault defaults = 1; + + // The minimum supported edition (inclusive) when this was constructed. + // Editions before this will not have defaults. + optional Edition minimum_edition = 4; + + // The maximum known edition (inclusive) when this was constructed. Editions + // after this will not have reliable defaults. + optional Edition maximum_edition = 5; +} + +// =================================================================== +// Optional source code info + +// Encapsulates information about the original source file from which a +// FileDescriptorProto was generated. +message SourceCodeInfo { + // A Location identifies a piece of source code in a .proto file which + // corresponds to a particular definition. This information is intended + // to be useful to IDEs, code indexers, documentation generators, and similar + // tools. + // + // For example, say we have a file like: + // message Foo { + // optional string foo = 1; + // } + // Let's look at just the field definition: + // optional string foo = 1; + // ^ ^^ ^^ ^ ^^^ + // a bc de f ghi + // We have the following locations: + // span path represents + // [a,i) [ 4, 0, 2, 0 ] The whole field definition. + // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). + // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). + // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). + // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). + // + // Notes: + // - A location may refer to a repeated field itself (i.e. not to any + // particular index within it). This is used whenever a set of elements are + // logically enclosed in a single code segment. For example, an entire + // extend block (possibly containing multiple extension definitions) will + // have an outer location whose path refers to the "extensions" repeated + // field without an index. + // - Multiple locations may have the same path. This happens when a single + // logical declaration is spread out across multiple places. The most + // obvious example is the "extend" block again -- there may be multiple + // extend blocks in the same scope, each of which will have the same path. + // - A location's span is not always a subset of its parent's span. For + // example, the "extendee" of an extension declaration appears at the + // beginning of the "extend" block and is shared by all extensions within + // the block. + // - Just because a location's span is a subset of some other location's span + // does not mean that it is a descendant. For example, a "group" defines + // both a type and a field in a single declaration. Thus, the locations + // corresponding to the type and field and their components will overlap. + // - Code which tries to interpret locations should probably be designed to + // ignore those that it doesn't understand, as more types of locations could + // be recorded in the future. + repeated Location location = 1; + message Location { + // Identifies which part of the FileDescriptorProto was defined at this + // location. + // + // Each element is a field number or an index. They form a path from + // the root FileDescriptorProto to the place where the definition occurs. + // For example, this path: + // [ 4, 3, 2, 7, 1 ] + // refers to: + // file.message_type(3) // 4, 3 + // .field(7) // 2, 7 + // .name() // 1 + // This is because FileDescriptorProto.message_type has field number 4: + // repeated DescriptorProto message_type = 4; + // and DescriptorProto.field has field number 2: + // repeated FieldDescriptorProto field = 2; + // and FieldDescriptorProto.name has field number 1: + // optional string name = 1; + // + // Thus, the above path gives the location of a field name. If we removed + // the last element: + // [ 4, 3, 2, 7 ] + // this path refers to the whole field declaration (from the beginning + // of the label to the terminating semicolon). + repeated int32 path = 1 [packed = true]; + + // Always has exactly three or four elements: start line, start column, + // end line (optional, otherwise assumed same as start line), end column. + // These are packed into a single field for efficiency. Note that line + // and column numbers are zero-based -- typically you will want to add + // 1 to each before displaying to a user. + repeated int32 span = 2 [packed = true]; + + // If this SourceCodeInfo represents a complete declaration, these are any + // comments appearing before and after the declaration which appear to be + // attached to the declaration. + // + // A series of line comments appearing on consecutive lines, with no other + // tokens appearing on those lines, will be treated as a single comment. + // + // leading_detached_comments will keep paragraphs of comments that appear + // before (but not connected to) the current element. Each paragraph, + // separated by empty lines, will be one comment element in the repeated + // field. + // + // Only the comment content is provided; comment markers (e.g. //) are + // stripped out. For block comments, leading whitespace and an asterisk + // will be stripped from the beginning of each line other than the first. + // Newlines are included in the output. + // + // Examples: + // + // optional int32 foo = 1; // Comment attached to foo. + // // Comment attached to bar. + // optional int32 bar = 2; + // + // optional string baz = 3; + // // Comment attached to baz. + // // Another line attached to baz. + // + // // Comment attached to moo. + // // + // // Another line attached to moo. + // optional double moo = 4; + // + // // Detached comment for corge. This is not leading or trailing comments + // // to moo or corge because there are blank lines separating it from + // // both. + // + // // Detached comment for corge paragraph 2. + // + // optional string corge = 5; + // /* Block comment attached + // * to corge. Leading asterisks + // * will be removed. */ + // /* Block comment attached to + // * grault. */ + // optional int32 grault = 6; + // + // // ignored detached comments. + optional string leading_comments = 3; + optional string trailing_comments = 4; + repeated string leading_detached_comments = 6; + } +} + +// Describes the relationship between generated code and its original source +// file. A GeneratedCodeInfo message is associated with only one generated +// source file, but may contain references to different source .proto files. +message GeneratedCodeInfo { + // An Annotation connects some span of text in generated code to an element + // of its generating .proto file. + repeated Annotation annotation = 1; + message Annotation { + // Identifies the element in the original source .proto file. This field + // is formatted the same as SourceCodeInfo.Location.path. + repeated int32 path = 1 [packed = true]; + + // Identifies the filesystem path to the original source .proto. + optional string source_file = 2; + + // Identifies the starting offset in bytes in the generated code + // that relates to the identified object. + optional int32 begin = 3; + + // Identifies the ending offset in bytes in the generated code that + // relates to the identified object. The end offset should be one past + // the last relevant byte (so the length of the text = end - begin). + optional int32 end = 4; + + // Represents the identified object's effect on the element in the original + // .proto file. + enum Semantic { + // There is no effect or the effect is indescribable. + NONE = 0; + // The element is set or otherwise mutated. + SET = 1; + // An alias to the element is returned. + ALIAS = 2; + } + optional Semantic semantic = 5; + } +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/duration.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/duration.proto new file mode 100644 index 00000000..41f40c22 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/duration.proto @@ -0,0 +1,115 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/durationpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DurationProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; + +// A Duration represents a signed, fixed-length span of time represented +// as a count of seconds and fractions of seconds at nanosecond +// resolution. It is independent of any calendar and concepts like "day" +// or "month". It is related to Timestamp in that the difference between +// two Timestamp values is a Duration and it can be added or subtracted +// from a Timestamp. Range is approximately +-10,000 years. +// +// # Examples +// +// Example 1: Compute Duration from two Timestamps in pseudo code. +// +// Timestamp start = ...; +// Timestamp end = ...; +// Duration duration = ...; +// +// duration.seconds = end.seconds - start.seconds; +// duration.nanos = end.nanos - start.nanos; +// +// if (duration.seconds < 0 && duration.nanos > 0) { +// duration.seconds += 1; +// duration.nanos -= 1000000000; +// } else if (duration.seconds > 0 && duration.nanos < 0) { +// duration.seconds -= 1; +// duration.nanos += 1000000000; +// } +// +// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. +// +// Timestamp start = ...; +// Duration duration = ...; +// Timestamp end = ...; +// +// end.seconds = start.seconds + duration.seconds; +// end.nanos = start.nanos + duration.nanos; +// +// if (end.nanos < 0) { +// end.seconds -= 1; +// end.nanos += 1000000000; +// } else if (end.nanos >= 1000000000) { +// end.seconds += 1; +// end.nanos -= 1000000000; +// } +// +// Example 3: Compute Duration from datetime.timedelta in Python. +// +// td = datetime.timedelta(days=3, minutes=10) +// duration = Duration() +// duration.FromTimedelta(td) +// +// # JSON Mapping +// +// In JSON format, the Duration type is encoded as a string rather than an +// object, where the string ends in the suffix "s" (indicating seconds) and +// is preceded by the number of seconds, with nanoseconds expressed as +// fractional seconds. For example, 3 seconds with 0 nanoseconds should be +// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should +// be expressed in JSON format as "3.000000001s", and 3 seconds and 1 +// microsecond should be expressed in JSON format as "3.000001s". +// +message Duration { + // Signed seconds of the span of time. Must be from -315,576,000,000 + // to +315,576,000,000 inclusive. Note: these bounds are computed from: + // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + int64 seconds = 1; + + // Signed fractions of a second at nanosecond resolution of the span + // of time. Durations less than one second are represented with a 0 + // `seconds` field and a positive or negative `nanos` field. For durations + // of one second or more, a non-zero value for the `nanos` field must be + // of the same sign as the `seconds` field. Must be from -999,999,999 + // to +999,999,999 inclusive. + int32 nanos = 2; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/empty.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/empty.proto new file mode 100644 index 00000000..b87c89dc --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/empty.proto @@ -0,0 +1,51 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option go_package = "google.golang.org/protobuf/types/known/emptypb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "EmptyProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; + +// A generic empty message that you can re-use to avoid defining duplicated +// empty messages in your APIs. A typical example is to use it as the request +// or the response type of an API method. For instance: +// +// service Foo { +// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); +// } +// +message Empty {} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/field_mask.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/field_mask.proto new file mode 100644 index 00000000..b28334b9 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/field_mask.proto @@ -0,0 +1,245 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option java_package = "com.google.protobuf"; +option java_outer_classname = "FieldMaskProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option go_package = "google.golang.org/protobuf/types/known/fieldmaskpb"; +option cc_enable_arenas = true; + +// `FieldMask` represents a set of symbolic field paths, for example: +// +// paths: "f.a" +// paths: "f.b.d" +// +// Here `f` represents a field in some root message, `a` and `b` +// fields in the message found in `f`, and `d` a field found in the +// message in `f.b`. +// +// Field masks are used to specify a subset of fields that should be +// returned by a get operation or modified by an update operation. +// Field masks also have a custom JSON encoding (see below). +// +// # Field Masks in Projections +// +// When used in the context of a projection, a response message or +// sub-message is filtered by the API to only contain those fields as +// specified in the mask. For example, if the mask in the previous +// example is applied to a response message as follows: +// +// f { +// a : 22 +// b { +// d : 1 +// x : 2 +// } +// y : 13 +// } +// z: 8 +// +// The result will not contain specific values for fields x,y and z +// (their value will be set to the default, and omitted in proto text +// output): +// +// +// f { +// a : 22 +// b { +// d : 1 +// } +// } +// +// A repeated field is not allowed except at the last position of a +// paths string. +// +// If a FieldMask object is not present in a get operation, the +// operation applies to all fields (as if a FieldMask of all fields +// had been specified). +// +// Note that a field mask does not necessarily apply to the +// top-level response message. In case of a REST get operation, the +// field mask applies directly to the response, but in case of a REST +// list operation, the mask instead applies to each individual message +// in the returned resource list. In case of a REST custom method, +// other definitions may be used. Where the mask applies will be +// clearly documented together with its declaration in the API. In +// any case, the effect on the returned resource/resources is required +// behavior for APIs. +// +// # Field Masks in Update Operations +// +// A field mask in update operations specifies which fields of the +// targeted resource are going to be updated. The API is required +// to only change the values of the fields as specified in the mask +// and leave the others untouched. If a resource is passed in to +// describe the updated values, the API ignores the values of all +// fields not covered by the mask. +// +// If a repeated field is specified for an update operation, new values will +// be appended to the existing repeated field in the target resource. Note that +// a repeated field is only allowed in the last position of a `paths` string. +// +// If a sub-message is specified in the last position of the field mask for an +// update operation, then new value will be merged into the existing sub-message +// in the target resource. +// +// For example, given the target message: +// +// f { +// b { +// d: 1 +// x: 2 +// } +// c: [1] +// } +// +// And an update message: +// +// f { +// b { +// d: 10 +// } +// c: [2] +// } +// +// then if the field mask is: +// +// paths: ["f.b", "f.c"] +// +// then the result will be: +// +// f { +// b { +// d: 10 +// x: 2 +// } +// c: [1, 2] +// } +// +// An implementation may provide options to override this default behavior for +// repeated and message fields. +// +// In order to reset a field's value to the default, the field must +// be in the mask and set to the default value in the provided resource. +// Hence, in order to reset all fields of a resource, provide a default +// instance of the resource and set all fields in the mask, or do +// not provide a mask as described below. +// +// If a field mask is not present on update, the operation applies to +// all fields (as if a field mask of all fields has been specified). +// Note that in the presence of schema evolution, this may mean that +// fields the client does not know and has therefore not filled into +// the request will be reset to their default. If this is unwanted +// behavior, a specific service may require a client to always specify +// a field mask, producing an error if not. +// +// As with get operations, the location of the resource which +// describes the updated values in the request message depends on the +// operation kind. In any case, the effect of the field mask is +// required to be honored by the API. +// +// ## Considerations for HTTP REST +// +// The HTTP kind of an update operation which uses a field mask must +// be set to PATCH instead of PUT in order to satisfy HTTP semantics +// (PUT must only be used for full updates). +// +// # JSON Encoding of Field Masks +// +// In JSON, a field mask is encoded as a single string where paths are +// separated by a comma. Fields name in each path are converted +// to/from lower-camel naming conventions. +// +// As an example, consider the following message declarations: +// +// message Profile { +// User user = 1; +// Photo photo = 2; +// } +// message User { +// string display_name = 1; +// string address = 2; +// } +// +// In proto a field mask for `Profile` may look as such: +// +// mask { +// paths: "user.display_name" +// paths: "photo" +// } +// +// In JSON, the same mask is represented as below: +// +// { +// mask: "user.displayName,photo" +// } +// +// # Field Masks and Oneof Fields +// +// Field masks treat fields in oneofs just as regular fields. Consider the +// following message: +// +// message SampleMessage { +// oneof test_oneof { +// string name = 4; +// SubMessage sub_message = 9; +// } +// } +// +// The field mask can be: +// +// mask { +// paths: "name" +// } +// +// Or: +// +// mask { +// paths: "sub_message" +// } +// +// Note that oneof type names ("test_oneof" in this case) cannot be used in +// paths. +// +// ## Field Mask Verification +// +// The implementation of any API method which has a FieldMask type field in the +// request should verify the included field paths, and return an +// `INVALID_ARGUMENT` error if any path is unmappable. +message FieldMask { + // The set of field mask paths. + repeated string paths = 1; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/source_context.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/source_context.proto new file mode 100644 index 00000000..135f50fe --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/source_context.proto @@ -0,0 +1,48 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option java_package = "com.google.protobuf"; +option java_outer_classname = "SourceContextProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option go_package = "google.golang.org/protobuf/types/known/sourcecontextpb"; + +// `SourceContext` represents information about the source of a +// protobuf element, like the file in which it is defined. +message SourceContext { + // The path-qualified name of the .proto file that contained the associated + // protobuf element. For example: `"google/protobuf/source_context.proto"`. + string file_name = 1; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/struct.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/struct.proto new file mode 100644 index 00000000..1bf0c1ad --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/struct.proto @@ -0,0 +1,95 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/structpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "StructProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; + +// `Struct` represents a structured data value, consisting of fields +// which map to dynamically typed values. In some languages, `Struct` +// might be supported by a native representation. For example, in +// scripting languages like JS a struct is represented as an +// object. The details of that representation are described together +// with the proto support for the language. +// +// The JSON representation for `Struct` is JSON object. +message Struct { + // Unordered map of dynamically typed values. + map fields = 1; +} + +// `Value` represents a dynamically typed value which can be either +// null, a number, a string, a boolean, a recursive struct value, or a +// list of values. A producer of value is expected to set one of these +// variants. Absence of any variant indicates an error. +// +// The JSON representation for `Value` is JSON value. +message Value { + // The kind of value. + oneof kind { + // Represents a null value. + NullValue null_value = 1; + // Represents a double value. + double number_value = 2; + // Represents a string value. + string string_value = 3; + // Represents a boolean value. + bool bool_value = 4; + // Represents a structured value. + Struct struct_value = 5; + // Represents a repeated `Value`. + ListValue list_value = 6; + } +} + +// `NullValue` is a singleton enumeration to represent the null value for the +// `Value` type union. +// +// The JSON representation for `NullValue` is JSON `null`. +enum NullValue { + // Null value. + NULL_VALUE = 0; +} + +// `ListValue` is a wrapper around a repeated field of values. +// +// The JSON representation for `ListValue` is JSON array. +message ListValue { + // Repeated field of dynamically typed values. + repeated Value values = 1; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/timestamp.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/timestamp.proto new file mode 100644 index 00000000..fd0bc07d --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/timestamp.proto @@ -0,0 +1,144 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/timestamppb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "TimestampProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; + +// A Timestamp represents a point in time independent of any time zone or local +// calendar, encoded as a count of seconds and fractions of seconds at +// nanosecond resolution. The count is relative to an epoch at UTC midnight on +// January 1, 1970, in the proleptic Gregorian calendar which extends the +// Gregorian calendar backwards to year one. +// +// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap +// second table is needed for interpretation, using a [24-hour linear +// smear](https://developers.google.com/time/smear). +// +// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By +// restricting to that range, we ensure that we can convert to and from [RFC +// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. +// +// # Examples +// +// Example 1: Compute Timestamp from POSIX `time()`. +// +// Timestamp timestamp; +// timestamp.set_seconds(time(NULL)); +// timestamp.set_nanos(0); +// +// Example 2: Compute Timestamp from POSIX `gettimeofday()`. +// +// struct timeval tv; +// gettimeofday(&tv, NULL); +// +// Timestamp timestamp; +// timestamp.set_seconds(tv.tv_sec); +// timestamp.set_nanos(tv.tv_usec * 1000); +// +// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. +// +// FILETIME ft; +// GetSystemTimeAsFileTime(&ft); +// UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; +// +// // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z +// // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. +// Timestamp timestamp; +// timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); +// timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); +// +// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. +// +// long millis = System.currentTimeMillis(); +// +// Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) +// .setNanos((int) ((millis % 1000) * 1000000)).build(); +// +// Example 5: Compute Timestamp from Java `Instant.now()`. +// +// Instant now = Instant.now(); +// +// Timestamp timestamp = +// Timestamp.newBuilder().setSeconds(now.getEpochSecond()) +// .setNanos(now.getNano()).build(); +// +// Example 6: Compute Timestamp from current time in Python. +// +// timestamp = Timestamp() +// timestamp.GetCurrentTime() +// +// # JSON Mapping +// +// In JSON format, the Timestamp type is encoded as a string in the +// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the +// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" +// where {year} is always expressed using four digits while {month}, {day}, +// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional +// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), +// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone +// is required. A proto3 JSON serializer should always use UTC (as indicated by +// "Z") when printing the Timestamp type and a proto3 JSON parser should be +// able to accept both UTC and other timezones (as indicated by an offset). +// +// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past +// 01:30 UTC on January 15, 2017. +// +// In JavaScript, one can convert a Date object to this format using the +// standard +// [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) +// method. In Python, a standard `datetime.datetime` object can be converted +// to this format using +// [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with +// the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use +// the Joda Time's [`ISODateTimeFormat.dateTime()`]( +// http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime() +// ) to obtain a formatter capable of generating timestamps in this format. +// +message Timestamp { + // Represents seconds of UTC time since Unix epoch + // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + // 9999-12-31T23:59:59Z inclusive. + int64 seconds = 1; + + // Non-negative fractions of a second at nanosecond resolution. Negative + // second values with fractions must still have non-negative nanos values + // that count forward in time. Must be from 0 to 999,999,999 + // inclusive. + int32 nanos = 2; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/type.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/type.proto new file mode 100644 index 00000000..48cb11e7 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/type.proto @@ -0,0 +1,193 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +import "google/protobuf/any.proto"; +import "google/protobuf/source_context.proto"; + +option cc_enable_arenas = true; +option java_package = "com.google.protobuf"; +option java_outer_classname = "TypeProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option go_package = "google.golang.org/protobuf/types/known/typepb"; + +// A protocol buffer message type. +message Type { + // The fully qualified message name. + string name = 1; + // The list of fields. + repeated Field fields = 2; + // The list of types appearing in `oneof` definitions in this type. + repeated string oneofs = 3; + // The protocol buffer options. + repeated Option options = 4; + // The source context. + SourceContext source_context = 5; + // The source syntax. + Syntax syntax = 6; + // The source edition string, only valid when syntax is SYNTAX_EDITIONS. + string edition = 7; +} + +// A single field of a message type. +message Field { + // Basic field types. + enum Kind { + // Field type unknown. + TYPE_UNKNOWN = 0; + // Field type double. + TYPE_DOUBLE = 1; + // Field type float. + TYPE_FLOAT = 2; + // Field type int64. + TYPE_INT64 = 3; + // Field type uint64. + TYPE_UINT64 = 4; + // Field type int32. + TYPE_INT32 = 5; + // Field type fixed64. + TYPE_FIXED64 = 6; + // Field type fixed32. + TYPE_FIXED32 = 7; + // Field type bool. + TYPE_BOOL = 8; + // Field type string. + TYPE_STRING = 9; + // Field type group. Proto2 syntax only, and deprecated. + TYPE_GROUP = 10; + // Field type message. + TYPE_MESSAGE = 11; + // Field type bytes. + TYPE_BYTES = 12; + // Field type uint32. + TYPE_UINT32 = 13; + // Field type enum. + TYPE_ENUM = 14; + // Field type sfixed32. + TYPE_SFIXED32 = 15; + // Field type sfixed64. + TYPE_SFIXED64 = 16; + // Field type sint32. + TYPE_SINT32 = 17; + // Field type sint64. + TYPE_SINT64 = 18; + } + + // Whether a field is optional, required, or repeated. + enum Cardinality { + // For fields with unknown cardinality. + CARDINALITY_UNKNOWN = 0; + // For optional fields. + CARDINALITY_OPTIONAL = 1; + // For required fields. Proto2 syntax only. + CARDINALITY_REQUIRED = 2; + // For repeated fields. + CARDINALITY_REPEATED = 3; + } + + // The field type. + Kind kind = 1; + // The field cardinality. + Cardinality cardinality = 2; + // The field number. + int32 number = 3; + // The field name. + string name = 4; + // The field type URL, without the scheme, for message or enumeration + // types. Example: `"type.googleapis.com/google.protobuf.Timestamp"`. + string type_url = 6; + // The index of the field type in `Type.oneofs`, for message or enumeration + // types. The first type has index 1; zero means the type is not in the list. + int32 oneof_index = 7; + // Whether to use alternative packed wire representation. + bool packed = 8; + // The protocol buffer options. + repeated Option options = 9; + // The field JSON name. + string json_name = 10; + // The string value of the default value of this field. Proto2 syntax only. + string default_value = 11; +} + +// Enum type definition. +message Enum { + // Enum type name. + string name = 1; + // Enum value definitions. + repeated EnumValue enumvalue = 2; + // Protocol buffer options. + repeated Option options = 3; + // The source context. + SourceContext source_context = 4; + // The source syntax. + Syntax syntax = 5; + // The source edition string, only valid when syntax is SYNTAX_EDITIONS. + string edition = 6; +} + +// Enum value definition. +message EnumValue { + // Enum value name. + string name = 1; + // Enum value number. + int32 number = 2; + // Protocol buffer options. + repeated Option options = 3; +} + +// A protocol buffer option, which can be attached to a message, field, +// enumeration, etc. +message Option { + // The option's name. For protobuf built-in options (options defined in + // descriptor.proto), this is the short name. For example, `"map_entry"`. + // For custom options, it should be the fully-qualified name. For example, + // `"google.api.http"`. + string name = 1; + // The option's value packed in an Any message. If the value is a primitive, + // the corresponding wrapper type defined in google/protobuf/wrappers.proto + // should be used. If the value is an enum, it should be stored as an int32 + // value using the google.protobuf.Int32Value type. + Any value = 2; +} + +// The syntax in which a protocol buffer element is defined. +enum Syntax { + // Syntax `proto2`. + SYNTAX_PROTO2 = 0; + // Syntax `proto3`. + SYNTAX_PROTO3 = 1; + // Syntax `editions`. + SYNTAX_EDITIONS = 2; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/wrappers.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/wrappers.proto new file mode 100644 index 00000000..1959fa55 --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/protobuf/wrappers.proto @@ -0,0 +1,123 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Wrappers for primitive (non-message) types. These types are useful +// for embedding primitives in the `google.protobuf.Any` type and for places +// where we need to distinguish between the absence of a primitive +// typed field and its default value. +// +// These wrappers have no meaningful use within repeated fields as they lack +// the ability to detect presence on individual elements. +// These wrappers have no meaningful use within a map or a oneof since +// individual entries of a map or fields of a oneof can already detect presence. + +syntax = "proto3"; + +package google.protobuf; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/wrapperspb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "WrappersProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; + +// Wrapper message for `double`. +// +// The JSON representation for `DoubleValue` is JSON number. +message DoubleValue { + // The double value. + double value = 1; +} + +// Wrapper message for `float`. +// +// The JSON representation for `FloatValue` is JSON number. +message FloatValue { + // The float value. + float value = 1; +} + +// Wrapper message for `int64`. +// +// The JSON representation for `Int64Value` is JSON string. +message Int64Value { + // The int64 value. + int64 value = 1; +} + +// Wrapper message for `uint64`. +// +// The JSON representation for `UInt64Value` is JSON string. +message UInt64Value { + // The uint64 value. + uint64 value = 1; +} + +// Wrapper message for `int32`. +// +// The JSON representation for `Int32Value` is JSON number. +message Int32Value { + // The int32 value. + int32 value = 1; +} + +// Wrapper message for `uint32`. +// +// The JSON representation for `UInt32Value` is JSON number. +message UInt32Value { + // The uint32 value. + uint32 value = 1; +} + +// Wrapper message for `bool`. +// +// The JSON representation for `BoolValue` is JSON `true` and `false`. +message BoolValue { + // The bool value. + bool value = 1; +} + +// Wrapper message for `string`. +// +// The JSON representation for `StringValue` is JSON string. +message StringValue { + // The string value. + string value = 1; +} + +// Wrapper message for `bytes`. +// +// The JSON representation for `BytesValue` is JSON string. +message BytesValue { + // The bytes value. + bytes value = 1; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/google/type/decimal.proto b/openfeature-provider/android/confidence-provider/src/main/proto/google/type/decimal.proto new file mode 100644 index 00000000..beb18a5d --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/google/type/decimal.proto @@ -0,0 +1,95 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.type; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/type/decimal;decimal"; +option java_multiple_files = true; +option java_outer_classname = "DecimalProto"; +option java_package = "com.google.type"; +option objc_class_prefix = "GTP"; + +// A representation of a decimal value, such as 2.5. Clients may convert values +// into language-native decimal formats, such as Java's [BigDecimal][] or +// Python's [decimal.Decimal][]. +// +// [BigDecimal]: +// https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/math/BigDecimal.html +// [decimal.Decimal]: https://docs.python.org/3/library/decimal.html +message Decimal { + // The decimal value, as a string. + // + // The string representation consists of an optional sign, `+` (`U+002B`) + // or `-` (`U+002D`), followed by a sequence of zero or more decimal digits + // ("the integer"), optionally followed by a fraction, optionally followed + // by an exponent. + // + // The fraction consists of a decimal point followed by zero or more decimal + // digits. The string must contain at least one digit in either the integer + // or the fraction. The number formed by the sign, the integer and the + // fraction is referred to as the significand. + // + // The exponent consists of the character `e` (`U+0065`) or `E` (`U+0045`) + // followed by one or more decimal digits. + // + // Services **should** normalize decimal values before storing them by: + // + // - Removing an explicitly-provided `+` sign (`+2.5` -> `2.5`). + // - Replacing a zero-length integer value with `0` (`.5` -> `0.5`). + // - Coercing the exponent character to lower-case (`2.5E8` -> `2.5e8`). + // - Removing an explicitly-provided zero exponent (`2.5e0` -> `2.5`). + // + // Services **may** perform additional normalization based on its own needs + // and the internal decimal implementation selected, such as shifting the + // decimal point and exponent value together (example: `2.5e-1` <-> `0.25`). + // Additionally, services **may** preserve trailing zeroes in the fraction + // to indicate increased precision, but are not required to do so. + // + // Note that only the `.` character is supported to divide the integer + // and the fraction; `,` **should not** be supported regardless of locale. + // Additionally, thousand separators **should not** be supported. If a + // service does support them, values **must** be normalized. + // + // The ENBF grammar is: + // + // DecimalString = + // [Sign] Significand [Exponent]; + // + // Sign = '+' | '-'; + // + // Significand = + // Digits ['.'] [Digits] | [Digits] '.' Digits; + // + // Exponent = ('e' | 'E') [Sign] Digits; + // + // Digits = { '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' }; + // + // Services **should** clearly document the range of supported values, the + // maximum supported precision (total number of digits), and, if applicable, + // the scale (number of digits after the decimal point), as well as how it + // behaves when receiving out-of-bounds values. + // + // Services **may** choose to accept values passed as input even when the + // value has a higher precision or scale than the service supports, and + // **should** round the value to fit the supported scale. Alternatively, the + // service **may** error with `400 Bad Request` (`INVALID_ARGUMENT` in gRPC) + // if precision would be lost. + // + // Services **should** error with `400 Bad Request` (`INVALID_ARGUMENT` in + // gRPC) if the service receives a value outside of the supported range. + string value = 1; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/proto/messages.proto b/openfeature-provider/android/confidence-provider/src/main/proto/messages.proto new file mode 100644 index 00000000..dc4c169f --- /dev/null +++ b/openfeature-provider/android/confidence-provider/src/main/proto/messages.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package rust_guest; + +import "google/protobuf/struct.proto"; + +option java_package = "rust_guest"; +option java_multiple_files = true; + +message Void {} + +message SetResolverStateRequest { + bytes state = 1; + string account_id = 2; +} + +message ResolveSimpleRequest { + string client_secret = 1; + google.protobuf.Struct evaluation_context = 2; + string name = 3; +} + + +message Request { + bytes data = 1; +} + +message Response { + oneof result { + bytes data = 1; + string error = 2; + } +} + +message LogMessage { + string message = 1; +} diff --git a/openfeature-provider/android/confidence-provider/src/main/resources/wasm/confidence_resolver.wasm b/openfeature-provider/android/confidence-provider/src/main/resources/wasm/confidence_resolver.wasm new file mode 100755 index 00000000..fa0b2bc1 Binary files /dev/null and b/openfeature-provider/android/confidence-provider/src/main/resources/wasm/confidence_resolver.wasm differ diff --git a/openfeature-provider/android/demo-app/build.gradle.kts b/openfeature-provider/android/demo-app/build.gradle.kts new file mode 100644 index 00000000..47796b78 --- /dev/null +++ b/openfeature-provider/android/demo-app/build.gradle.kts @@ -0,0 +1,86 @@ +import java.util.Properties + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +// Load local.properties for secrets +val localProperties = Properties().apply { + val localPropertiesFile = rootProject.file("local.properties") + if (localPropertiesFile.exists()) { + load(localPropertiesFile.inputStream()) + } +} + +android { + namespace = "com.spotify.confidence.demo" + compileSdk = 34 + + defaultConfig { + applicationId = "com.spotify.confidence.demo" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + // Read client secret from local.properties + buildConfigField( + "String", + "CONFIDENCE_CLIENT_SECRET", + "\"${localProperties.getProperty("confidence.clientSecret", "")}\"" + ) + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + viewBinding = true + buildConfig = true + } + + packaging { + resources { + // Exclude proto files that conflict with protobuf-javalite + excludes += "google/protobuf/*.proto" + excludes += "google/api/*.proto" + excludes += "google/type/*.proto" + } + } +} + +dependencies { + implementation(project(":confidence-provider")) + + // OpenFeature SDK for Android + implementation(libs.openfeature.android) + + // Android core + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + + // Coroutines + implementation(libs.bundles.coroutines) +} diff --git a/openfeature-provider/android/demo-app/src/main/AndroidManifest.xml b/openfeature-provider/android/demo-app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f471c7ac --- /dev/null +++ b/openfeature-provider/android/demo-app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/openfeature-provider/android/demo-app/src/main/kotlin/com/spotify/confidence/demo/MainActivity.kt b/openfeature-provider/android/demo-app/src/main/kotlin/com/spotify/confidence/demo/MainActivity.kt new file mode 100644 index 00000000..3539edb3 --- /dev/null +++ b/openfeature-provider/android/demo-app/src/main/kotlin/com/spotify/confidence/demo/MainActivity.kt @@ -0,0 +1,372 @@ +package com.spotify.confidence.demo + +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import com.spotify.confidence.android.ConfidenceLocalProvider +import com.spotify.confidence.android.LocalProviderConfig +import com.spotify.confidence.demo.databinding.ActivityMainBinding +import dev.openfeature.kotlin.sdk.ImmutableContext +import dev.openfeature.kotlin.sdk.OpenFeatureAPI +import dev.openfeature.kotlin.sdk.Value +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong + +class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMainBinding + private val logBuilder = StringBuilder() + private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.US) + + // Benchmark state + private var benchmarkJob: Job? = null + private val isBenchmarkRunning = AtomicBoolean(false) + private val totalRequests = AtomicInteger(0) + private val totalErrors = AtomicInteger(0) + private val totalLatencyNanos = AtomicLong(0) + private val minLatencyNanos = AtomicLong(Long.MAX_VALUE) + private val maxLatencyNanos = AtomicLong(0) + private var benchmarkStartTime = 0L + private val latencyHistogram = mutableListOf() // Store latencies for percentile calculation + + companion object { + private const val TAG = "ConfidenceDemo" + // Set via BuildConfig from local.properties: confidence.clientSecret=YOUR_SECRET + private const val CLIENT_SECRET = BuildConfig.CONFIDENCE_CLIENT_SECRET + private const val FLAG_KEY = "mattias-boolean-flag.enabled" + private const val UI_UPDATE_INTERVAL_MS = 100L + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setupButtons() + log("Demo app started") + log("Client Secret: ${CLIENT_SECRET.take(8)}...") + log("Flag to resolve: $FLAG_KEY") + } + + private fun setupButtons() { + binding.initButton.setOnClickListener { + initializeProvider() + } + + binding.resolveButton.setOnClickListener { + resolveFlag() + } + + binding.benchmarkButton.setOnClickListener { + if (isBenchmarkRunning.get()) { + stopBenchmark() + } else { + startBenchmark() + } + } + } + + private fun initializeProvider() { + binding.initButton.isEnabled = false + binding.statusText.text = "Status: Initializing..." + log("Initializing provider...") + + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + val config = LocalProviderConfig.Builder() + .build() + + val provider = ConfidenceLocalProvider.create( + clientSecret = CLIENT_SECRET, + config = config + ) + + log("Provider created: ${provider.metadata.name}") + + val context = ImmutableContext( + targetingKey = "vahid", + attributes = mapOf( + "user_id" to Value.String("vahid"), + "country" to Value.String("SE"), + "platform" to Value.String("android"), + "app_version" to Value.String("1.0.0") + ) + ) + + log("Setting evaluation context: targeting_key=vahid") + OpenFeatureAPI.setEvaluationContext(context) + + log("Calling provider.initialize()...") + provider.initialize(context) + + OpenFeatureAPI.setProvider(provider) + log("Provider set successfully!") + } + + withContext(Dispatchers.Main) { + binding.statusText.text = "Status: Ready" + binding.statusText.setTextColor(getColor(android.R.color.holo_green_dark)) + binding.resolveButton.isEnabled = true + binding.benchmarkButton.isEnabled = true + binding.initButton.text = "Re-init" + binding.initButton.isEnabled = true + log("Provider initialization complete!") + } + + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize", e) + withContext(Dispatchers.Main) { + binding.statusText.text = "Status: Error" + binding.statusText.setTextColor(getColor(android.R.color.holo_red_dark)) + binding.initButton.isEnabled = true + log("ERROR: ${e.message}") + } + } + } + } + + private fun resolveFlag() { + binding.resolveButton.isEnabled = false + log("Resolving flag: $FLAG_KEY") + + lifecycleScope.launch { + try { + val client = OpenFeatureAPI.getClient() + val startTime = System.currentTimeMillis() + + val evaluation = withContext(Dispatchers.IO) { + client.getBooleanDetails(FLAG_KEY, false) + } + + val duration = System.currentTimeMillis() - startTime + + withContext(Dispatchers.Main) { + binding.flagValueText.text = "Value: ${evaluation.value}" + binding.variantText.text = "Variant: ${evaluation.variant ?: "N/A"}" + binding.reasonText.text = "Reason: ${evaluation.reason ?: "N/A"}" + + val color = if (evaluation.value) { + getColor(android.R.color.holo_green_dark) + } else { + getColor(android.R.color.holo_red_dark) + } + binding.flagValueText.setTextColor(color) + + log("=== RESOLUTION RESULT ===") + log("Value: ${evaluation.value}, Variant: ${evaluation.variant}, Duration: ${duration}ms") + log("=========================") + + binding.resolveButton.isEnabled = true + } + + } catch (e: Exception) { + Log.e(TAG, "Failed to resolve flag", e) + withContext(Dispatchers.Main) { + binding.flagValueText.text = "Value: ERROR" + binding.flagValueText.setTextColor(getColor(android.R.color.holo_red_dark)) + log("ERROR resolving flag: ${e.message}") + binding.resolveButton.isEnabled = true + } + } + } + } + + private fun startBenchmark() { + // Reset stats + totalRequests.set(0) + totalErrors.set(0) + totalLatencyNanos.set(0) + minLatencyNanos.set(Long.MAX_VALUE) + maxLatencyNanos.set(0) + synchronized(latencyHistogram) { + latencyHistogram.clear() + } + benchmarkStartTime = System.nanoTime() + + isBenchmarkRunning.set(true) + binding.benchmarkButton.text = "STOP" + binding.benchmarkButton.setBackgroundColor(getColor(android.R.color.holo_red_dark)) + binding.benchmarkCard.visibility = View.VISIBLE + binding.resolveButton.isEnabled = false + binding.initButton.isEnabled = false + + log("=== BENCHMARK STARTED ===") + log("Press STOP to end the benchmark") + + // Start the benchmark worker + benchmarkJob = lifecycleScope.launch { + val client = OpenFeatureAPI.getClient() + + // UI update job + val uiUpdateJob = launch { + while (isActive && isBenchmarkRunning.get()) { + updateBenchmarkUI() + delay(UI_UPDATE_INTERVAL_MS) + } + } + + // Benchmark worker - run continuously until stopped + withContext(Dispatchers.IO) { + while (isBenchmarkRunning.get()) { + val startNanos = System.nanoTime() + try { + client.getBooleanDetails(FLAG_KEY, false) + val latencyNanos = System.nanoTime() - startNanos + + totalRequests.incrementAndGet() + totalLatencyNanos.addAndGet(latencyNanos) + + // Update min/max atomically + var current = minLatencyNanos.get() + while (latencyNanos < current) { + if (minLatencyNanos.compareAndSet(current, latencyNanos)) break + current = minLatencyNanos.get() + } + + current = maxLatencyNanos.get() + while (latencyNanos > current) { + if (maxLatencyNanos.compareAndSet(current, latencyNanos)) break + current = maxLatencyNanos.get() + } + + // Store for percentile calculation (limit to last 10000 samples) + synchronized(latencyHistogram) { + if (latencyHistogram.size >= 10000) { + latencyHistogram.removeAt(0) + } + latencyHistogram.add(latencyNanos) + } + + } catch (e: Exception) { + totalErrors.incrementAndGet() + } + } + } + + uiUpdateJob.cancel() + } + } + + private fun stopBenchmark() { + isBenchmarkRunning.set(false) + benchmarkJob?.cancel() + + binding.benchmarkButton.text = "Benchmark" + binding.benchmarkButton.setBackgroundColor(getColor(android.R.color.holo_blue_dark)) + binding.resolveButton.isEnabled = true + binding.initButton.isEnabled = true + + // Final UI update + updateBenchmarkUI() + binding.benchmarkStatusText.text = "Stopped" + binding.benchmarkStatusText.setTextColor(getColor(android.R.color.darker_gray)) + + // Log final results + val requests = totalRequests.get() + val errors = totalErrors.get() + val elapsedSeconds = (System.nanoTime() - benchmarkStartTime) / 1_000_000_000.0 + val rps = if (elapsedSeconds > 0) requests / elapsedSeconds else 0.0 + val avgLatencyMs = if (requests > 0) (totalLatencyNanos.get() / requests) / 1_000_000.0 else 0.0 + + val (p50, p95, p99) = calculatePercentiles() + + log("=== BENCHMARK COMPLETED ===") + log("Total Requests: $requests") + log("Total Errors: $errors") + log("Duration: %.2f seconds".format(elapsedSeconds)) + log("RPS: %.1f".format(rps)) + log("Avg Latency: %.2f ms".format(avgLatencyMs)) + log("Min Latency: %.2f ms".format(minLatencyNanos.get() / 1_000_000.0)) + log("Max Latency: %.2f ms".format(maxLatencyNanos.get() / 1_000_000.0)) + log("P50: %.2f ms, P95: %.2f ms, P99: %.2f ms".format(p50, p95, p99)) + log("===========================") + } + + private fun updateBenchmarkUI() { + runOnUiThread { + val requests = totalRequests.get() + val errors = totalErrors.get() + val elapsedNanos = System.nanoTime() - benchmarkStartTime + val elapsedSeconds = elapsedNanos / 1_000_000_000.0 + + // Calculate RPS + val rps = if (elapsedSeconds > 0) requests / elapsedSeconds else 0.0 + + // Calculate average latency + val avgLatencyMs = if (requests > 0) { + (totalLatencyNanos.get() / requests) / 1_000_000.0 + } else { + 0.0 + } + + // Calculate percentiles + val (p50, p95, p99) = calculatePercentiles() + + // Update UI + binding.totalRequestsText.text = "%,d".format(requests) + binding.rpsText.text = "%.1f".format(rps) + binding.avgLatencyText.text = "%.2f ms".format(avgLatencyMs) + + val minMs = if (minLatencyNanos.get() == Long.MAX_VALUE) 0.0 else minLatencyNanos.get() / 1_000_000.0 + val maxMs = maxLatencyNanos.get() / 1_000_000.0 + binding.minMaxLatencyText.text = "%.1f / %.1f ms".format(minMs, maxMs) + + binding.percentilesText.text = "%.1f / %.1f / %.1f ms".format(p50, p95, p99) + binding.errorsText.text = errors.toString() + + if (isBenchmarkRunning.get()) { + binding.benchmarkStatusText.text = "Running... (%.1fs)".format(elapsedSeconds) + binding.benchmarkStatusText.setTextColor(getColor(android.R.color.holo_blue_dark)) + } + } + } + + private fun calculatePercentiles(): Triple { + synchronized(latencyHistogram) { + if (latencyHistogram.isEmpty()) { + return Triple(0.0, 0.0, 0.0) + } + + val sorted = latencyHistogram.sorted() + val size = sorted.size + + val p50Index = (size * 0.50).toInt().coerceIn(0, size - 1) + val p95Index = (size * 0.95).toInt().coerceIn(0, size - 1) + val p99Index = (size * 0.99).toInt().coerceIn(0, size - 1) + + return Triple( + sorted[p50Index] / 1_000_000.0, + sorted[p95Index] / 1_000_000.0, + sorted[p99Index] / 1_000_000.0 + ) + } + } + + private fun log(message: String) { + val timestamp = dateFormat.format(Date()) + val logLine = "[$timestamp] $message\n" + Log.d(TAG, message) + + runOnUiThread { + logBuilder.append(logLine) + // Keep log size manageable + if (logBuilder.length > 5000) { + logBuilder.delete(0, logBuilder.length - 4000) + } + binding.logText.text = logBuilder.toString() + } + } +} diff --git a/openfeature-provider/android/demo-app/src/main/res/drawable/ic_launcher_foreground.xml b/openfeature-provider/android/demo-app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..e15b0eea --- /dev/null +++ b/openfeature-provider/android/demo-app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/openfeature-provider/android/demo-app/src/main/res/layout/activity_main.xml b/openfeature-provider/android/demo-app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..4c107cee --- /dev/null +++ b/openfeature-provider/android/demo-app/src/main/res/layout/activity_main.xml @@ -0,0 +1,339 @@ + + + + + + + + + +